Convert Geometry Types in PyQGIS
Geometry types are strict in QGIS. A polygon layer accepts polygons, a LineStringZ layer expects a Z value on every vertex, and a shapefile written as Polygon rejects the multipolygon that a dissolve just produced. Converting between types is therefore a routine part of moving data between layers, formats and tools: promoting singles to multi before an append, turning parcel boundaries into lines for a road-frontage measure, collapsing buildings to points for a density map, or stripping elevation from a survey before it goes into a 2D database.
This recipe belongs to Features, Geometries & Memory Layers. It covers each common conversion for a single QgsGeometry, then the Processing algorithms that do the same for a whole layer.
Prerequisites
- QGIS 3.34 LTR or newer, or the QGIS 4 series.
- A clear idea of the target layer's geometry type.
QgsWkbTypes.displayString(layer.wkbType())prints it, for exampleMultiPolygonZ.
Single part and multipart
The most frequent conversion is between single and multipart within the same family. It is lossless in one direction and potentially lossy in the other.
from qgis.core import QgsGeometry, QgsWkbTypes
single = QgsGeometry.fromWkt("Polygon ((0 0, 10 0, 10 10, 0 10, 0 0))")
single.convertToMultiType()
print(single.asWkt()) # MultiPolygon (((0 0, 10 0, ...)))
islands = QgsGeometry.fromWkt(
"MultiPolygon (((0 0, 4 0, 4 4, 0 4, 0 0)), ((10 0, 14 0, 14 4, 10 4, 10 0)))")
parts = [QgsGeometry(p.clone()) for p in islands.constParts()]
print(len(parts), "single parts")
ok = islands.convertToSingleType() # keeps only the first part!
print(ok, islands.asWkt())
Breakdown: convertToMultiType() wraps the geometry in a one-member collection in place and always succeeds. Going the other way, convertToSingleType() succeeds even when there are several parts — and silently discards all but the first. That is almost never what you want; iterate constParts() and create one feature per part instead, or use native:multiparttosingleparts for a layer as shown in converting multipart to singlepart. Promoting everything to multi is the safe default when writing to a layer, which is why many workflows create every output layer as a Multi type.
Polygons to lines and lines to polygons
A polygon's outline is a line; a closed line encloses a polygon. QGIS converts in both directions, with one condition for the second.
from qgis.core import QgsGeometry, QgsLineString, QgsPolygon
parcel = QgsGeometry.fromWkt(
"Polygon ((0 0, 40 0, 40 30, 0 30, 0 0), (10 10, 20 10, 20 20, 10 20, 10 10))")
outline = QgsGeometry(parcel.constGet().boundary())
print(outline.asWkt()) # MultiLineString ((0 0, 40 0, ...), (10 10, ...))
print("perimeter incl. hole:", outline.length())
trace = QgsGeometry.fromWkt("LineString (0 0, 50 0, 50 20, 0 20)")
line = trace.constGet().clone()
if not line.isClosed():
line.addVertex(line.startPoint()) # close the ring
poly = QgsGeometry(QgsPolygon(line))
print(poly.asWkt(), poly.isGeosValid())
Breakdown: boundary() lives on the abstract geometry (constGet()), returning a new object that QgsGeometry takes ownership of. For a polygon with holes the result is multipart: one line per ring, so its length is the full perimeter including the hole edges — use parcel.constGet().exteriorRing() for the outer edge alone. Creating a polygon from a line requires a closed ring; closing an open trace by appending the start point is correct when the ends nearly meet and produces a self-intersecting shape when they do not, so check isGeosValid() afterwards. For lines that form a network of edges rather than single rings, native:polygonize builds polygons from every enclosed face.
Any shape to points
Points stand for features in density maps, labelling and joins. Which point depends on the question being asked.
building = QgsGeometry.fromWkt(
"Polygon ((0 0, 30 0, 30 10, 10 10, 10 30, 0 30, 0 0))") # an L-shape
centroid = building.centroid() # may fall outside the shape
inside = building.pointOnSurface() # always inside
pole = building.poleOfInaccessibility(0.5)[0] # most central interior point
vertices = [QgsGeometry(v) for v in building.vertices()]
print(centroid.asWkt(1), building.contains(centroid))
print(inside.asWkt(1), pole.asWkt(1), len(vertices), "vertices")
Breakdown: The centroid of an L-shaped or crescent polygon can lie outside it, which breaks point-in-polygon joins and puts labels in the wrong place. pointOnSurface() guarantees an interior point; poleOfInaccessibility() finds the interior point furthest from any edge, which is the visually central point and what QGIS uses for polygon labels. Vertex points are useful for inspecting digitizing quality or snapping. For whole layers, native:centroids with ALL_PARTS, native:pointonsurface and native:extractvertices do the same.
Curves to straight segments
Curved geometries — circular arcs in CircularString, CompoundCurve and CurvePolygon — come from CAD data, some national cadastres and QGIS's own curve digitizing. Many formats and tools cannot store them.
from qgis.core import QgsGeometry, QgsAbstractGeometry
arc_road = QgsGeometry.fromWkt(
"CompoundCurve (CircularString (0 0, 50 50, 100 0), (100 0, 160 0))")
print(arc_road.isMultipart(), arc_road.constGet().hasCurvedSegments())
coarse = arc_road.constGet().segmentize(0.2, QgsAbstractGeometry.MaximumAngle)
fine = arc_road.constGet().segmentize(0.01, QgsAbstractGeometry.MaximumAngle)
print(coarse.nCoordinates(), "vs", fine.nCoordinates(), "vertices")
straight = QgsGeometry(fine) # LineString, ready for any format
print(straight.asWkt(1)[:60], "…")
Breakdown: segmentize approximates arcs with straight segments. With MaximumAngle, the tolerance is the angle in radians subtended by each segment; with MaximumDifference, it is the maximum distance between arc and chord in layer units, which is easier to relate to data accuracy — a centimetre on a cadastre. Curves are worth keeping where the target supports them (GeoPackage and PostGIS do); convert only at the boundary with a format that does not, such as shapefile or GeoJSON.
Add or drop Z and M
Elevation and measure values must match the target layer: a 2D table rejects Z values on some providers and silently drops them on others, while a PointZ layer fills missing Z with NaN or zero.
from qgis.core import QgsGeometry
pt = QgsGeometry.fromWkt("Point (571200 5934000)")
geom3d = QgsGeometry(pt.constGet().clone())
geom3d.get().addZValue(0.0)
print(geom3d.asWkt()) # Point Z (571200 5934000 0)
survey = QgsGeometry.fromWkt("LineString Z (0 0 410, 10 0 412, 20 0 415)")
flat = QgsGeometry(survey.constGet().clone())
flat.get().dropZValue()
print(flat.asWkt()) # LineString (0 0, 10 0, 20 0)
Breakdown: get() returns a mutable pointer to the abstract geometry, on which addZValue, dropZValue, addMValue and dropMValue work in place. Adding a constant Z is rarely the end of the story — real elevations come from a DEM via native:setzfromraster (Drape) for a whole layer. Dropping Z before writing to a 2D target is preferable to letting the provider decide. Cloning first keeps the original intact, which matters when the same geometry is used elsewhere in the script.
Convert a whole layer with Processing
For layers, the Processing algorithms are faster than a Python loop and handle attributes for you. Most single-geometry conversions above have a layer equivalent.
import processing
steps = {
"promote": ("native:promotetomulti", {}),
"boundary": ("native:boundary", {}),
"points": ("native:pointonsurface", {"ALL_PARTS": False}),
"straight": ("native:segmentizebymaxangle", {"ANGLE": 1.0}),
"flatten": ("native:dropmzvalues", {"DROP_Z_VALUES": True, "DROP_M_VALUES": True}),
}
alg, extra = steps["boundary"]
result = processing.run(alg, {"INPUT": "/data/parcels.gpkg|layername=parcels",
"OUTPUT": "memory:", **extra})["OUTPUT"]
print(result.wkbType(), result.featureCount())
converted = processing.run("qgis:convertgeometrytype", {
"INPUT": result, "TYPE": 2, "OUTPUT": "memory:"})["OUTPUT"] # 2 = Linestrings
Breakdown: Each algorithm keeps the attributes and writes one output feature per input feature (or per part, where noted). Running the conversion in Processing also records it in the Processing history, so the exact parameters can be reviewed or repeated later, and invalid input geometry is handled according to the global invalid-feature setting rather than crashing the loop halfway through a large layer. native:promotetomulti is the bulk version of convertToMultiType and should precede appends into a multi-typed target. qgis:convertgeometrytype is the general converter, choosing among centroids, nodes, linestrings, multilinestrings and polygons by an enum. Chaining several conversions through memory layers follows the pattern in using memory layers between algorithms.
QGIS version compatibility
All methods shown exist in QGIS 3.x and QGIS 4. poleOfInaccessibility and native:pointonsurface require 3.0 and later; native:dropmzvalues and native:segmentizebymaxangle arrived in 3.0 as well. On QGIS 4, the QgsAbstractGeometry.MaximumAngle enum is spelled QgsAbstractGeometry.SegmentationToleranceType.MaximumAngle.
Troubleshooting
- An append fails with a type mismatch. The source has singles and the target is multi, or vice versa; promote with
convertToMultiType()ornative:promotetomulti. - Only one part survives conversion to single.
convertToSingleType()drops the rest; split parts into separate features instead. - The polygon from a closed line is invalid. The ring self-intersects; check
isGeosValid()and repair withmakeValid(). - Shapefile export fails on curves. Segmentize first; shapefiles cannot store arcs.
Conclusion
Use convertToMultiType freely and convertToSingleType only for genuinely single geometries, take boundary() for outlines and close rings before building polygons, pick pointOnSurface or poleOfInaccessibility when a point must lie inside, segmentize curves only at format boundaries, match Z and M to the target, and use the Processing equivalents for whole layers.
Frequently Asked Questions
Why does QGIS create Multi layers by default? Because a multi-typed layer accepts both single and multipart geometry, which avoids type errors after operations like dissolve.
How do I convert points into a line in visit order?
Sort by a sequence field and use native:pointstopath, which builds one line per group.
Is converting to 2D lossy? Yes. Elevation values are discarded; keep the 3D source if you may need them later.
How do I turn lines into polygons when they form a network rather than closed rings?
Use native:polygonize, which nodes the lines and creates a polygon for every enclosed face. It is the right tool for parcel boundaries digitized as separate line segments, where no single line forms a closed ring on its own.
Can I convert a polygon to its convex hull?
Yes: geom.convexHull(), or native:convexhull for a layer.