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.

The same coastline at three tolerancesA detailed coastline with many vertices is shown at tolerance zero with every vertex marked. At a small tolerance about half the vertices remain and the shape is visually identical. At a large tolerance only a handful of vertices remain and the outline noticeably departs from the original, which is drawn dashed behind it.Tolerance is the maximum distance a dropped vertex may be from the kept lineoriginal · 24 verticesevery survey point keptsimplify(8) · 8 verticesvisually identical, 3× smallersimplify(40) · 3 verticesshape is now a caricature

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.

Why independent simplification breaks shared boundariesOn the left, two adjacent polygons share a jagged boundary. In the middle, each has been simplified on its own and the two versions of the shared boundary no longer coincide, leaving a gap on one side and an overlap on the other. On the right, topology-preserving simplification produces one shared boundary used identically by both polygons.Simplifying neighbours independently pulls their shared edge apartoriginal neighboursone shared boundarysimplified separatelyslivertwo boundaries that disagreetopology preservedone boundary, used by both

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.

Stored simplification versus render-time simplificationOn the left, a source layer is simplified once and written to disk, so both the analysis path and the rendering path consume the reduced geometry. On the right, the full-precision layer is kept and only the rendering path applies simplification, so analysis still sees every vertex.Only one of these keeps your measurements exactsimplify and storesourcesimplifiedon diskanalysis tooareas and lengths now approximatesimplify at render timefull precisionon diskreduced for drawinganalysis exactnothing on disk is altered

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 versionPythonNotes
3.28 LTR3.9Identical API and algorithm parameters.
3.34 LTR3.12Baseline for this page.
3.40 / 3.443.12QgsGeometry.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:fixgeometries afterwards, or switch to the area method.
  • Attributes are missing from the rebuilt polygons. native:polygonize does 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.