Applying a Categorized Renderer from Python

Build a QgsCategorizedSymbolRenderer in PyQGIS: provider-side uniqueValues, one symbol per class, stable legend ordering, an explicit fallback for unplanned…

TL;DR: ask the provider for the distinct values of the classifying field, build one symbol per value, wrap each in a QgsRendererCategory, hand the list to QgsCategorizedSymbolRenderer and call triggerRepaint() — and always give unplanned values a visible fallback rather than letting them disappear. This page is part of the layer styling and symbology automation guide.

Complete Runnable Code

python
"""Categorize a vector layer by one field, with an explicit fallback class."""
from qgis.core import (QgsCategorizedSymbolRenderer, QgsRendererCategory,
                       QgsSymbol, QgsVectorLayer)
from qgis.PyQt.QtGui import QColor

FALLBACK = "#9e9e9e"


def symbol_for(layer: QgsVectorLayer, colour: str) -> QgsSymbol:
    """A default symbol for this layer's geometry type, tinted to `colour`."""
    symbol = QgsSymbol.defaultSymbol(layer.geometryType())
    symbol.setColor(QColor(colour))
    return symbol


def categorize(layer: QgsVectorLayer, field: str, palette: dict[str, str],
               include_null: bool = True) -> int:
    """Style `layer` with one class per distinct value of `field`.

    Values absent from `palette` are drawn in grey and labelled, so an
    unexpected class is visible on the map rather than silently missing.
    Returns the number of classes created.
    """
    index = layer.fields().indexFromName(field)
    if index < 0:
        raise KeyError("no field %r on %r" % (field, layer.name()))

    values = layer.uniqueValues(index)          # a provider-side DISTINCT
    categories = []
    for value in sorted(v for v in values if v is not None):
        colour = palette.get(str(value), FALLBACK)
        categories.append(
            QgsRendererCategory(value, symbol_for(layer, colour), str(value)))

    if include_null and None in values:
        categories.append(
            QgsRendererCategory(None, symbol_for(layer, FALLBACK), "no value"))

    renderer = QgsCategorizedSymbolRenderer(field, categories)
    layer.setRenderer(renderer)
    layer.triggerRepaint()
    return len(categories)
From distinct values to a styled layer A four-step path: ask the provider for the distinct values of the classifying field, build one symbol per value, assemble them into renderer categories, and assign the finished renderer to the layer. uniqueValues() asked of the provider one symbol each cloned, then tinted QgsRendererCategory value, symbol, label setRenderer() then repaint distinct per value assemble

Architecture Breakdown

uniqueValues() — let the provider do the counting

uniqueValues(index) asks the data provider for the distinct set. On a database backend that is one SELECT DISTINCT; on a file backend it is a single scan performed in C++. The obvious alternative — iterating every feature in Python and collecting values into a set — does the same work an order of magnitude more slowly, and it fetches every attribute and geometry along the way.

The method returns a set, so ordering is not defined. Sorting before building the categories is what makes the legend stable between runs, which matters more than it sounds: an unsorted legend reorders itself every time the script runs, and every map that embeds it changes with no explanation.

QgsRendererCategory — value, symbol, label

Each category binds one attribute value to one symbol and one legend label. The value is compared with the feature’s attribute using QGIS’s own type-aware comparison, which is why passing the raw value from uniqueValues() is more reliable than converting it to a string first.

The label is what appears in the legend and is free text. Using the raw value there is fine while exploring; a production map usually wants a human-readable name, which means the palette is better modelled as value to (colour, label) rather than value to colour alone.

Three ways to source the class colours A grid of three colour sources — a fixed palette dictionary, a QGIS colour ramp, and randomly generated colours — showing what each guarantees and where each is appropriate. guarantees appropriate when fixed palette the same class is the same colour the classes are known colour ramp a visually even spread the classes are ordered random colours nothing at all exploring unfamiliar data

setRenderer() — ownership and repainting

setRenderer() takes ownership of the renderer object. Passing the same instance to two layers is a bug: one of them ends up holding an object that has already been claimed. When applying one style across many layers, pass renderer.clone() each time.

Nothing repaints on its own. triggerRepaint() refreshes the canvas; inside a plugin you usually also want iface.layerTreeView().refreshLayerSymbology(layer.id()) so the legend matches what the map now shows.

Handling Values the Palette Does Not Cover

Any classification built from a fixed palette eventually meets a value nobody planned for, and what the code does at that moment is a design decision rather than an accident.

What to do with a value you did not plan for A decision tree for unexpected class values: draw them with an explicit fallback symbol, group them into an "other" class, or fail the run when an unknown class means the input is wrong. A value appeared that the palette does not cover exploring grey fallback visible, not hidden production map an "other" class labelled honestly controlled input fail the run the data is wrong

The default behaviour of a categorized renderer is unkind: a feature whose value matches no category is not drawn at all. On a map of land parcels, a new zoning code introduced upstream simply removes those parcels from the map, and nothing anywhere reports it.

The fallback class in the example above is the minimum defence — unknown values are grey and labelled, so they are visibly present and visibly unclassified. In a pipeline over controlled input, the stronger option is to treat an unknown value as an error: if the vocabulary is fixed by an agreement, a value outside it means the input is wrong and publishing a map from it is the last thing you want.

The middle path, an explicit “other” class, suits public-facing cartography where the map must render regardless but honesty about what is uncategorised still matters.

Applying It Across a Project

python
from qgis.core import QgsProject, QgsVectorLayer

ZONING = {"residential": "#8bc34a", "commercial": "#03a9f4", "industrial": "#ff9800"}


def style_all_zoning_layers() -> int:
    """Apply the zoning palette to every project layer carrying a zone field."""
    styled = 0
    for layer in QgsProject.instance().mapLayers().values():
        if not isinstance(layer, QgsVectorLayer):
            continue
        if layer.fields().indexFromName("zone") < 0:
            continue
        categorize(layer, "zone", ZONING)
        styled += 1
    return styled

Selecting layers by the presence of a field rather than by name is worth the extra line: it keeps working when somebody renames a layer, which they will.

Production Best Practices

  • Sort the categories. An unsorted legend changes order between runs for no reason anyone can see.
  • Always define a fallback. Silently undrawn features are the worst failure mode a map has.
  • Clone the renderer per layer. Sharing one instance across layers is undefined behaviour.
  • Handle NULL explicitly. It is a distinct value and a distinct legend entry, not an absence.
  • Keep the palette in one place, ideally with the labels, so the same class is the same colour in every map your team produces.
  • Refresh the legend, not just the canvas, or the two will disagree until the user clicks something.

Frequently Asked Questions

Why are some features missing after I categorize?

Because their value matched no category. A categorized renderer draws nothing for an unmatched feature — it does not fall back to a default symbol. Add an explicit fallback category, or check uniqueValues() against your palette before styling and fail loudly on the difference.

How do I keep colours stable when the data changes?

Drive them from a fixed mapping of value to colour rather than from a ramp applied in whatever order the values arrived. A ramp assigns colours by position, so adding one new class shifts every colour after it, and last month’s map no longer matches this month’s.

Can I categorize on an expression instead of a field?

Yes — QgsCategorizedSymbolRenderer accepts an expression string where a field name would go, so "substr(\"code\", 1, 1)" classifies by the first character. The cost is that the expression is evaluated per feature per render, so for anything hot it is worth writing the derived value into a real column instead.

Does the order of categories matter for drawing?

It does, and not only for the legend. Categories are drawn in list order, so later classes paint over earlier ones where features overlap. On a polygon layer with overlapping parcels, reordering the categories changes which colour is visible on top.

Sorting alphabetically, as the example does, is a reasonable default because it is predictable. When one class genuinely needs to sit above the others — a highlight class, or an alert state — put it last deliberately rather than hoping the sort happens to do it.

What is the difference between this and a rule-based renderer?

A categorized renderer maps one attribute to a flat list of classes. A rule-based renderer evaluates arbitrary expressions, can nest rules, and can bind rules to scale ranges. Use the categorized form while the styling really is one field to one symbol — it is easier to read and much easier to generate — and escalate only when the condition genuinely needs more.