Control Label Placement and Collisions in PyQGIS
Labelling is the part of automated cartography that most often looks wrong, and almost always for the same reason: QGIS could not fit every label, so it dropped some — silently, and not the ones you would have dropped. The engine is doing exactly what it was told. Telling it something better is a handful of properties: where labels may sit, which features matter more, what counts as an obstacle, and when overlapping is preferable to disappearing.
This recipe belongs to Labeling and Annotations. It covers placement modes per geometry type, priority, obstacles, allowing overlap deliberately, callout lines for crowded areas, and how to find out why a specific label is missing.
Prerequisites
- QGIS 3.34 LTR (bundled Python 3.12) or newer.
- A layer with labels already enabled — see Add Rule-Based Labels in PyQGIS for the labelling basics.
- A crowded extent, which is where any of this becomes visible.
Set the placement mode
Placement is a property of the label settings, and the sensible value depends entirely on geometry type.
from qgis.core import (QgsPalLayerSettings, QgsVectorLayerSimpleLabeling,
QgsTextFormat, Qgis)
settings = QgsPalLayerSettings()
settings.fieldName = "name"
settings.enabled = True
settings.placement = Qgis.LabelPlacement.OverPoint # points
settings.quadOffset = Qgis.LabelQuadrantPosition.AboveRight
settings.dist = 2.0
layer.setLabeling(QgsVectorLayerSimpleLabeling(settings))
layer.setLabelsEnabled(True)
layer.triggerRepaint()
Breakdown: For points, OverPoint with a quadrant offset places the label at a fixed position relative to the symbol, which produces a tidy, predictable map; AroundPoint lets the engine try several positions and fit more labels at the cost of consistency. For lines, Line follows the geometry — with Curved bending the text along it, as covered in Curved Labels Along Lines in PyQGIS — and Horizontal keeps text upright, which reads better for short line features. For polygons, AroundPoint at the centroid suits small shapes, Horizontal suits large ones, and PerimeterCurved suits long thin ones such as rivers. dist is the gap between the feature and its label in the current units.
Decide who wins a collision
settings.priority = 8 # 0 lowest, 10 highest
settings.obstacleSettings().setIsObstacle(True)
settings.obstacleSettings().setFactor(1.5)
Breakdown: Priority decides the order in which labels are placed, and therefore which one survives when two want the same space — a layer of towns at priority 9 keeps its labels while a layer of hamlets at priority 3 loses its overlapping ones, which is the correct cartographic outcome and needs no manual editing. The obstacle settings control whether features of this layer block other layers' labels: a building footprint layer set as an obstacle stops street names being written across buildings. The factor weights how strongly the obstacle repels, so a value above one makes the engine work harder to avoid it. Priority is per label setting, so a rule-based labelling can give different priorities to different classes of the same layer.
Allow overlap when a missing label is worse
settings.obstacleSettings().setIsObstacle(False)
settings.displayAll = True # draw every label, overlapping if necessary
Breakdown: displayAll switches off collision detection for this layer, so every feature gets a label whether or not it collides. That sounds like a mistake and is occasionally exactly right: on a technical plan where every parcel reference must appear, an overlapping label the reader can untangle beats a missing one they cannot. The honest version of this decision is usually to reduce what is labelled instead — filter the features, or label only above a certain scale — because a map with a hundred overlapping labels communicates less than one with twenty placed well. Use it deliberately, and never on a layer where the labels are decorative.
Label only what belongs at this scale
settings.scaleVisibility = True
settings.minimumScale = 250000 # not labelled when zoomed further out
settings.maximumScale = 5000 # not labelled when zoomed further in
settings.fieldName = "name"
settings.isExpression = False
settings.setDataDefinedProperty(
QgsPalLayerSettings.Show,
QgsProperty.fromExpression("\"population\" > 10000"))
Breakdown: Scale-based label visibility is the cleanest solution to overcrowding: label towns at regional scales and hamlets only when zoomed in, and the map is readable at every scale without any collision drama. Remember that the minimum scale is the zoomed-out limit, which reads backwards until you think in scale denominators. The data-defined Show property goes further, deciding per feature whether to label at all — an expression here is far better than a filter on the layer, because the unlabelled features still draw. Together these two produce the "label fewer things" strategy, which is nearly always the right answer to a crowded map.
Add callouts instead of moving labels away
When a label must sit away from its feature, a callout line keeps the association clear.
from qgis.core import QgsSimpleLineCallout, QgsProperty
callout = QgsSimpleLineCallout()
callout.setEnabled(True)
callout.setMinimumLength(2.0)
callout.lineSymbol().setWidth(0.3)
settings.setCallout(callout)
settings.placement = Qgis.LabelPlacement.AroundPoint
settings.dist = 6.0
Breakdown: A callout draws a thin line from the label to its feature, which lets labels be pushed out of a dense cluster while remaining unambiguous — the technique every published map of a crowded coastline uses. setMinimumLength() suppresses the line when the label is already close enough that a line would be visual noise. Increasing dist alongside gives the engine room to move labels outward, which is what makes the callout useful in the first place. Callouts cost render time and clutter, so they are for the crowded parts of a map rather than a default.
Find out why a label is missing
The engine gives no warning, so diagnosis is a short checklist worth running in order.
Is the feature labelled at all? Check the field for NULL or an empty string — a feature with no value is not a missing label, it is a feature with nothing to say.
Is it excluded by scale or a data-defined Show? Both fail silently by design. Print the settings, or temporarily disable them.
Is another layer blocking it? Turn off obstacles on the other layers one at a time. Buildings and polygon fills are the usual culprits.
Is it losing a collision? Raise the priority to 10 temporarily. If the label appears, it was being outranked, and the real fix is a considered priority ordering across layers rather than everything at 10.
Is it simply too big to fit? A long name at a large font in a small polygon has no valid position anywhere. Shorten it with an expression, wrap it with settings.autoWrapLength, or let it sit outside the polygon with a callout.
Working through those five in order settles nearly every case, and the order matters because the cheap checks are first.
QGIS version compatibility
| QGIS version | Python | Notes |
|---|---|---|
| 3.22 LTR | 3.9 | Placement and obstacle settings present; enumerations live on QgsPalLayerSettings rather than Qgis. |
| 3.28 LTR | 3.9 | Both enumeration spellings available. |
| 3.34 LTR | 3.12 | Baseline for this page; scoped Qgis.LabelPlacement names preferred. |
| 3.40 / 3.44 | 3.12 | Identical behaviour; legacy enumeration aliases are being removed, so prefer the scoped names. |
If a placement constant raises an AttributeError, check which spelling your release uses with dir(Qgis) and dir(QgsPalLayerSettings) — the introspection approach in Explore the PyQGIS API with dir() and help() settles it immediately.
Troubleshooting
- No labels at all.
setLabelsEnabled(True)was not called, orsettings.enabledisFalse. Both are required. - Some labels missing at every zoom. Collisions. Raise priority, reduce obstacles, or label fewer features.
- Labels disappear when zooming in. A maximum scale is set, or a data-defined
Showexpression is excluding them. - Labels are drawn over the wrong features. Obstacle settings on the other layers. Mark solid layers as obstacles.
- Curved labels on lines look broken. The line is too short or too sharply bent for the text. Fall back to horizontal placement for short features.
- Everything overlaps.
displayAllis on. It is an escape hatch, not a default.
Conclusion
Choose a placement mode that suits the geometry, set priorities so the important layer wins collisions, and mark solid layers as obstacles so labels avoid them. Prefer labelling fewer features — by scale rule or a data-defined Show expression — over allowing everything to overlap, use callouts where labels must sit away from their feature, and when a label is missing work through the five checks in order rather than changing settings at random.
Frequently Asked Questions
Why does QGIS drop labels instead of shrinking them? Because a variable font size makes a map harder to read, not easier. Where shrinking is acceptable, a data-defined size expression can do it explicitly.
Can I place a label at an exact position? Yes — data-defined position properties take X and Y from fields, which is how manually placed labels are stored. It is the right tool for a small number of stubborn labels.
How do I stop labels being cut off at the map frame? Enable label margins on the layout map item, so labels near the edge are drawn inside the frame rather than clipped.
Do priorities work across layers? Yes — the whole map is labelled by one engine pass, so priority ranks labels from every layer against each other.
Is there a way to see which labels were dropped?
Not directly. Compare the labelled count against the feature count by rendering with displayAll temporarily and looking at the difference.
Should labels be a layer property or a layout decision? The layer holds the labelling; a layout map can override it through a map theme, which is how one project produces a busy screen map and a clean printed one — see Add a Map Item and Set Its Extent in PyQGIS.