Data-Defined Symbol Size and Colour in PyQGIS

Drive QGIS symbol size and colour from attribute values with QgsProperty and setDataDefinedProperty: which level the override attaches to, the per-feature…

TL;DR: attach a QgsProperty built from an expression to a specific property of a specific symbol layer with setDataDefinedProperty() — the expression is evaluated once per feature per render, so use it for genuinely continuous variation and precompute the value when the layer is rendered often. This page is part of the layer styling and symbology automation guide.

Complete Runnable Code

python
"""Drive marker size and fill colour from attribute values."""
from qgis.core import QgsProperty, QgsSymbolLayer, QgsVectorLayer


def size_by_field(layer: QgsVectorLayer, field: str,
                  lo_mm: float = 2.0, hi_mm: float = 12.0) -> None:
    """Scale marker size linearly with `field`, clamped between `lo_mm` and `hi_mm`."""
    symbol_layer = layer.renderer().symbol().symbolLayer(0)
    expression = (
        'coalesce(scale_linear("{f}", minimum("{f}"), maximum("{f}"), {lo}, {hi}), {lo})'
    ).format(f=field, lo=lo_mm, hi=hi_mm)
    symbol_layer.setDataDefinedProperty(
        QgsSymbolLayer.PropertySize, QgsProperty.fromExpression(expression))
    layer.triggerRepaint()


def colour_by_threshold(layer: QgsVectorLayer, field: str, threshold: float) -> None:
    """Colour features red above `threshold`, blue below it, grey when unknown."""
    expression = (
        'CASE WHEN "{f}" IS NULL THEN \'#9e9e9e\' '
        "WHEN \"{f}\" > {t} THEN '#c62828' ELSE '#1565c0' END"
    ).format(f=field, t=threshold)
    symbol_layer = layer.renderer().symbol().symbolLayer(0)
    symbol_layer.setDataDefinedProperty(
        QgsSymbolLayer.PropertyFillColor, QgsProperty.fromExpression(expression))
    layer.triggerRepaint()

The coalesce in the size expression is not defensive clutter. scale_linear returns NULL when its input is NULL, and a NULL size renders as nothing at all — so without it, features with no value silently vanish from the map.

Where a data-defined override attaches Three bands showing where an override lives: the renderer chooses a symbol, the symbol holds symbol layers, and the override is attached to one property of one symbol layer where it is evaluated per feature. Renderer picks the symbol per feature Symbol symbol layer 0 the fill symbol layer 1 the outline Property PropertySize an expression PropertyFillColor an expression An override set on the symbol rather than on a symbol layer silently does nothing — the property lives one level further down.

Architecture Breakdown

QgsProperty — an expression bound to one property

QgsProperty.fromExpression() wraps an expression string in an object the symbol layer can evaluate. There is also fromField(), which is a faster path when the value needs no arithmetic at all: it reads the column directly instead of running the expression engine.

Every property has an enum constant — PropertySize, PropertyFillColor, PropertyStrokeWidth, PropertyAngle and so on — and the constants belong to QgsSymbolLayer. Setting a property that the symbol layer type does not support is silently ignored, which is the second most common reason an override appears to do nothing.

Why it must go on the symbol layer

A symbol is a container; the properties that are painted belong to the symbol layers inside it. Code that reaches for symbol.setDataDefinedProperty() does not fail — that method exists for a small set of symbol-level properties — but the size and colour overrides you want are one level down, on symbol.symbolLayer(0).

For a symbol with several layers, choose deliberately: an override on layer 0 changes the fill and leaves the outline alone, which is usually right, but not always what a reader expects.

The cost model

Every data-defined property is an expression evaluated once per feature, on every render pass. On a canvas the user is panning around, that is several times a second.

What each override costs per render A bar chart of full-extent render time for a 150 000-point layer with no override, one size override, size and colour overrides, and overrides whose expressions call an aggregate function. no override 210 ms size override 480 ms size + colour 760 ms aggregate in expression 6.4 s Indicative full-extent renders of 150 000 points. An aggregate function is evaluated per feature and scans the layer each time, which is why it dominates so completely.

The last bar is the important one. An expression containing an aggregate — maximum("value"), sum("area") — is re-evaluated per feature, and each evaluation scans the layer. The example above avoids this by computing the bounds once in Python and interpolating them into the expression as literals, which turns a quadratic cost into a linear one.

Choosing Between an Override and Classes

Before reaching for an override it is worth asking whether the property really varies continuously, because the cheaper answers cover more cases than they get credit for.

Override, classes, or a precomputed column? A decision tree on how a varying property should be driven: classes when a handful of discrete steps suffice, a data-defined override for genuinely continuous variation, and a precomputed column when the same value is rendered repeatedly. How does this property vary? a few steps use classes cheapest to render continuously data-defined override expression per feature continuously, often precompute a column then override on it

Overrides and renderer classes solve overlapping problems, and the choice is mostly about how many distinct outcomes there are and how often the layer is drawn. Half a dozen size steps read perfectly well as a graduated renderer, which costs one classification lookup per feature instead of an expression evaluation. Genuinely continuous variation — a proportional symbol map — is what overrides are for.

The third option deserves more use than it gets: precompute the derived value into a real column with the field calculation pattern, then bind the override to that column with QgsProperty.fromField(). The arithmetic happens once in a batch rather than on every repaint, and the map behaves like an unstyled one.

Testing an Override Before Shipping It

An override is easy to get subtly wrong in a way that only shows up on real data, so it is worth a deliberate check rather than a glance at the canvas. Evaluate the expression in the field calculator over the actual layer and look at the distribution of results: how many rows produced NULL, what the minimum and maximum came out as, and whether the range makes sense in millimetres or as a colour.

Then render the full extent once and time it. A property that costs three milliseconds per thousand features is invisible on a test file and unusable on a production one, and the only way to find out which you have is to draw all of it.

Production Best Practices

  • Wrap size expressions in coalesce. A NULL size draws nothing, with no warning.
  • Never put an aggregate in an override. It scans the layer once per feature.
  • Interpolate bounds as literals rather than recomputing minimum and maximum per feature.
  • Set the property on a symbol layer, not on the symbol.
  • Prefer fromField() to fromExpression() when no arithmetic is needed.
  • Check the render time after adding an override, on a full extent, before shipping it. The cost is invisible on a test file of a thousand features and obvious on a real one.

Frequently Asked Questions

Which properties can be overridden?

Most of the ones you would expect, and the list depends on the symbol layer type. A simple marker exposes size, angle, fill and stroke colour, stroke width and offset; a simple fill exposes fill and stroke colour, stroke width and style; a line symbol layer adds dash pattern and offset. The enum constants all live on QgsSymbolLayer, and the safest way to discover what a given symbol layer supports is to inspect it in the interface, where unsupported properties simply do not appear.

Setting a constant the symbol layer does not support is not an error — it is stored and ignored, which makes it one of the harder styling mistakes to spot.

Why did my override have no effect?

Three usual causes: it was set on the symbol rather than the symbol layer, the property constant does not apply to that symbol layer type, or the expression returns NULL for every feature. None of the three raises — the map simply looks unchanged.

Test the expression in the field calculator first. If it produces sensible values there, the problem is where the override was attached rather than what it computes.

Can an override read a field that is not in the renderer?

Yes. The expression is evaluated against a context with the feature scope, so any field on the layer is available regardless of what the renderer classifies on. Bear in mind that QGIS must therefore fetch that attribute for every feature it draws, which is a cost worth knowing about on wide tables.

How do I make symbol size proportional to a value rather than linear with it?

For proportional symbols the area, not the diameter, should be proportional to the value — so the size expression needs a square root. scale_exp with an exponent of 0.5 does this directly, and using a linear scale instead is the classic mistake that makes a map exaggerate large values.

Can I combine an override with a categorized renderer?

Yes, and it is a common combination: the renderer picks a colour per class, and an override on each class’s symbol scales the size by a value. Set the override on the symbol layer of each category’s symbol, not once on the renderer, because each category owns its own symbol.

That is also the moment to consider whether the classification is still earning its place. Two overrides on a single symbol will sometimes express the same map more simply than a dozen classes each carrying its own override.

Do overrides survive a QML export?

Yes. Data-defined properties are part of the symbol layer definition and are written into the QML along with everything else. They do not survive an SLD export, which has no vocabulary for them — another reason to precompute values into columns when the style must reach a map server.