Simplify Geometry in PyQGIS
Simplification removes vertices that contribute little to a shape. It is what turns a coastline digitised at survey precision into something that renders at 1:2 000 000 without melting the browser, and what shrinks a GeoJSON payload from eighty megabytes to three. It is also destructive: vertices are gone, areas shift slightly, and — the failure that catches people — adjacent polygons simplified independently no longer share a boundary, leaving visible gaps and slivers between them.
This page is a focused recipe within Geometry Operations and Spatial Predicates in PyQGIS. It covers the Douglas-Peucker method and its tolerance, measuring the effect before committing, preserving topology across neighbours, and choosing between simplification and generalisation.
Prerequisites
- QGIS 3.34 LTR (bundled Python 3.12) or newer.
- A layer in a projected CRS, because the tolerance is expressed in map units and a tolerance in degrees is almost never what you want.
- Valid geometry — simplification of a self-intersecting polygon can produce a worse self-intersection. Repair first with Find and Fix Invalid Geometries.
Simplify one geometry
simplify() implements Douglas-Peucker: it keeps the endpoints, finds the vertex furthest from the line joining them, and recurses on either side while that distance exceeds the tolerance.
from qgis.core import QgsProject
layer = QgsProject.instance().mapLayersByName("coastline")[0]
geometry = next(layer.getFeatures()).geometry()
before = geometry.constGet().vertexCount()
simplified = geometry.simplify(10.0)
after = simplified.constGet().vertexCount()
print(f"{before} -> {after} vertices ({100 * (1 - after / before):.1f}% removed)")
Breakdown: The tolerance is in map units, so 10.0 in a metric CRS means no dropped vertex is more than ten metres from the retained line. constGet().vertexCount() is the honest measure of how much was removed — file size and render time both track it closely. Printing the percentage rather than the raw numbers makes it easy to compare tolerances quickly.
Choosing a tolerance is a rendering decision, and the useful rule of thumb ties it to the scale the data will be drawn at: a vertex closer than half a pixel to its neighbour cannot be seen. At 1:50 000 on a 96 DPI screen, one pixel is roughly 13 m on the ground, so a tolerance around 6 m is invisible.
def tolerance_for_scale(scale, dpi=96, pixels=0.5):
"""Ground distance corresponding to a fraction of a pixel at a given scale."""
metres_per_pixel = (0.0254 / dpi) * scale
return metres_per_pixel * pixels
print(round(tolerance_for_scale(50000), 1), "m at 1:50 000")
print(round(tolerance_for_scale(2000000), 1), "m at 1:2 000 000")
Breakdown: The chain is pixels → inches (divide by DPI) → metres on paper (× 0.0254) → metres on the ground (× the scale denominator) — the same conversion used for map scale and DPI when exporting images. Deriving the tolerance rather than guessing it means the simplification is provably invisible at the target scale.
Simplify a whole layer
The Processing algorithm handles the iteration and offers two additional methods beyond Douglas-Peucker.
import processing
processing.run("native:simplifygeometries", {
"INPUT": "/data/coastline.gpkg|layername=coastline",
"METHOD": 0, # 0 distance (Douglas-Peucker), 1 snap to grid, 2 area (Visvalingam)
"TOLERANCE": 10.0,
"OUTPUT": "/data/output/coastline_simple.gpkg",
})
Breakdown: Distance (0) is Douglas-Peucker and preserves the extremes of a shape, which suits coastlines and boundaries. Snap to grid (1) rounds coordinates onto a regular grid, which is blunt but produces coordinates that compress extremely well. Area (2) is Visvalingam-Whyatt, which removes the vertices forming the smallest triangles and tends to look more natural on smooth curves — it degrades more gracefully at aggressive tolerances than Douglas-Peucker, which can produce visible spikes.
Preserve shared boundaries
This is the failure that turns a tidy simplification into a day of repair work. Two polygons that share a boundary, simplified independently, keep different subsets of the shared vertices — so the boundary no longer matches and gaps and overlaps appear along it.
QGIS's own simplification does not preserve topology across features. When the shared boundary matters — administrative areas, land parcels, anything that tiles a surface — convert the polygons to their boundary lines, simplify those once, and rebuild the polygons:
import processing
lines = processing.run("native:polygonstolines", {
"INPUT": "/data/districts.gpkg|layername=districts",
"OUTPUT": "TEMPORARY_OUTPUT",
})["OUTPUT"]
simple_lines = processing.run("native:simplifygeometries", {
"INPUT": lines, "METHOD": 0, "TOLERANCE": 25.0,
"OUTPUT": "TEMPORARY_OUTPUT",
})["OUTPUT"]
polygons = processing.run("native:polygonize", {
"INPUT": simple_lines, "KEEP_FIELDS": False,
"OUTPUT": "/data/output/districts_simple.gpkg",
})["OUTPUT"]
Breakdown: Each shared boundary segment appears once in the line layer, so simplifying it once guarantees both neighbours end up with the identical edge. native:polygonize rebuilds closed areas from the line network. The cost is that attributes do not survive the round trip — KEEP_FIELDS: False acknowledges that — so the result has to be re-joined to the original attributes by a spatial relationship, typically with native:joinattributesbylocation using the original centroids. For heavy generalisation of administrative boundaries, dedicated tools such as GRASS v.generalize with its topology support are worth the extra dependency.
Simplify for display only
Simplification is destructive, and often what you actually want is a faster render rather than a smaller dataset. QGIS can simplify at draw time, leaving the stored geometry untouched — full precision for analysis, few vertices for the screen.
from qgis.core import QgsProject, QgsVectorSimplifyMethod
layer = QgsProject.instance().mapLayersByName("coastline")[0]
method = QgsVectorSimplifyMethod()
method.setSimplifyHints(QgsVectorSimplifyMethod.GeometrySimplification)
method.setSimplifyAlgorithm(QgsVectorSimplifyMethod.Distance)
method.setThreshold(1.0) # in pixels, not map units
method.setForceLocalOptimization(True)
layer.setSimplifyMethod(method)
layer.triggerRepaint()
Breakdown: setThreshold() here is in screen pixels, not map units — a threshold of 1.0 drops any vertex that would land within a pixel of its neighbour at the current zoom, which is by definition invisible. Because the tolerance is relative to the display, the simplification automatically becomes gentler as the user zooms in. setForceLocalOptimization(True) lets QGIS simplify in the provider where supported, so the vertices are never even transferred. Nothing about the stored data changes, so measureArea() and every predicate still see the full-precision geometry.
Use render-time simplification whenever the goal is a responsive canvas, and stored simplification only when the deliverable itself needs to be smaller — a web tile set, a mobile export, an API payload.
QGIS version compatibility
The examples target QGIS 3.34 LTR (Python 3.12).
| QGIS version | Python | Notes |
|---|---|---|
| 3.28 LTR | 3.9 | Identical API and algorithm parameters. |
| 3.34 LTR | 3.12 | Baseline for this page. |
| 3.40 / 3.44 | 3.12 | QgsGeometry.simplifyCoverageVW() adds coverage-aware simplification, removing the need for the lines round trip. |
simplify() and native:simplifygeometries are unchanged across 3.x. If you are on 3.40 or newer and simplifying a polygon coverage, prefer the coverage-aware method.
Troubleshooting
- Nothing was removed. The tolerance is smaller than the spacing between vertices. Raise it, or check the CRS — a tolerance of 10 in EPSG:4326 means ten degrees, which flattens everything to a straight line.
- The shape collapsed to a line or vanished. The tolerance exceeded the shape's own dimensions. Compare
vertexCount()before and after and back the tolerance off. - Gaps and slivers between neighbours. Each polygon was simplified independently. Use the lines round trip above, or the coverage-aware method on 3.40+.
- Self-intersections appeared. Douglas-Peucker can create them at aggressive tolerances on convoluted shapes. Run
native:fixgeometriesafterwards, or switch to the area method. - Attributes are missing from the rebuilt polygons.
native:polygonizedoes not carry attributes. Re-join them by location using the original centroids. - File size barely changed. Vertex count is not the only cost — check whether the layer's size is dominated by attributes rather than coordinates.
Conclusion
Simplification is a scale decision expressed as a tolerance: derive it from the map scale the data will be drawn at rather than guessing, verify the effect by comparing vertex counts, and pick Douglas-Peucker for boundaries and the area method for smooth curves. Whenever polygons share edges, simplify the shared boundaries once rather than each polygon independently.
Frequently Asked Questions
What tolerance should I use? Derive it from the target map scale. A vertex closer than half a pixel to the retained line is invisible, so at 1:50 000 and 96 DPI a tolerance around 6 m is imperceptible. Guessing tends to be either too timid to help or aggressive enough to distort shapes.
Why did gaps appear between my polygons? They were simplified independently, so each kept a different subset of the shared boundary's vertices. Convert to lines, simplify once, and polygonize — or use the coverage-aware simplification available in QGIS 3.40 and newer.
Which simplification method is best? Distance (Douglas-Peucker) preserves the extremes of a shape and suits boundaries and coastlines. Area (Visvalingam) removes the least significant vertices by triangle area and looks more natural on smooth curves at aggressive tolerances.
Does simplifying change the area of a polygon? Yes, slightly. Douglas-Peucker does not preserve area, so a simplified polygon's measured area differs from the original. If area must be preserved exactly, keep the original geometry for measurement and use the simplified copy only for rendering.