Graduated and Categorized Renderers in PyQGIS
Thematic mapping turns raw attribute tables into maps that communicate patterns at a glance. The two renderers at the heart of this work are QgsCategorizedSymbolRenderer, which assigns one symbol per distinct attribute value, and QgsGraduatedSymbolRenderer, which slices a continuous numeric field into classified ranges. Mastering both — along with the classification methods and color ramps that drive them — lets you generate consistent, reproducible cartography entirely from code. This cluster sits inside the PyQGIS Cartography & Data Visualization pillar and focuses specifically on data-driven symbol assignment: how QGIS decides which feature gets which color or size, and how you control that decision programmatically.
Where Programmatic Layer Styling deals with single symbols and per-feature properties, and Labeling & Annotations deals with text, this page is about classification — grouping features and mapping each group to a visual variable.
Both renderers share the same underlying model: a layer holds exactly one renderer, and that renderer owns a collection of class definitions plus an optional color ramp. A categorized renderer's collection is a list of QgsRendererCategory objects; a graduated renderer's is a list of QgsRendererRange objects. Understanding that shared structure is what lets you write generic styling code that branches only where the two genuinely differ — discovering categories versus computing numeric breaks. The sections below build that understanding step by step, from creating each renderer to inspecting, recoloring, and persisting it.
Prerequisites
- QGIS 3.34 LTR (bundles Python 3.12) or a comparable 3.x release with PyQGIS available.
- A vector layer loaded in the project with at least one categorical field (e.g.
land_use) and one numeric field (e.g.population). - Access to the QGIS Python Console (
Ctrl+Alt+P) or a standalone script with an initializedQgsApplication. - Familiarity with
QgsVectorLayer,QgsSymbol, and how to retrieve layers viaQgsProject.instance().
Categorized vs Graduated at a Glance
The single most common mistake is reaching for the wrong renderer. Categorized renderers answer "what kind is it?" (nominal data), while graduated renderers answer "how much of it is there?" (ordinal or ratio data). The diagram below contrasts how each maps source data to symbols.
1. Categorizing a Layer by a Unique Attribute Value
A categorized renderer iterates the distinct values of a field and binds each to its own QgsSymbol. The clean modern path is QgsCategorizedSymbolRenderer.createCategories(), which collects the unique values, generates symbols from a template, and applies a color ramp in one call.
from qgis.core import (
QgsProject,
QgsCategorizedSymbolRenderer,
QgsSymbol,
QgsStyle,
)
layer = QgsProject.instance().mapLayersByName("land_cover")[0]
field_name = "land_use"
# Template symbol carries geometry-appropriate defaults (fill, stroke).
template_symbol = QgsSymbol.defaultSymbol(layer.geometryType())
# Pick a qualitative ramp so unordered categories read as distinct.
default_style = QgsStyle.defaultStyle()
color_ramp = default_style.colorRamp("Spectral")
categories = QgsCategorizedSymbolRenderer.createCategories(
[], template_symbol, layer, field_name
)
renderer = QgsCategorizedSymbolRenderer(field_name, categories)
renderer.updateColorRamp(color_ramp)
layer.setRenderer(renderer)
layer.triggerRepaint()
Breakdown: createCategories([], ...) with an empty value list tells QGIS to scan the layer and discover every distinct value of land_use. QgsSymbol.defaultSymbol() returns a fill, line, or marker symbol matching the layer geometry, so the same code works for polygons or points. updateColorRamp() recolors all categories from the ramp in evenly spaced steps. Always call triggerRepaint() (or iface.mapCanvas().refresh()) so the change is visible.
For nominal data, prefer a qualitative ramp such as Spectral or Set1 rather than a sequential one — sequential ramps imply an order that categories do not have.
If you need to constrain the categories to a known set rather than discovering every value, pass that list explicitly instead of an empty one. This is useful when a field contains noise values you want to exclude, or when you want categories to appear in a deliberate order in the legend:
# Only build categories for the three classes you care about,
# in the order they should appear in the legend.
wanted = ["forest", "urban", "water"]
categories = QgsCategorizedSymbolRenderer.createCategories(
wanted, template_symbol, layer, field_name
)
renderer = QgsCategorizedSymbolRenderer(field_name, categories)
renderer.updateColorRamp(QgsStyle.defaultStyle().colorRamp("Set1"))
layer.setRenderer(renderer)
Breakdown: Supplying a non-empty value list short-circuits discovery, so QGIS builds one category per item you named and skips everything else. Any feature whose value is not in the list falls through to the renderer's "all other values" entry, which you can enable, hide, or restyle separately. This gives you full control over both membership and legend order, which automatic discovery (alphabetical by default) does not guarantee.
2. Adjusting Individual Categories
After building a categorized renderer you frequently need to recolor one class, rename a label, or hide a value. Each entry is a QgsRendererCategory you can read and rewrite in place.
from qgis.PyQt.QtGui import QColor
renderer = layer.renderer() # QgsCategorizedSymbolRenderer
for i, category in enumerate(renderer.categories()):
value = category.value()
label = category.label()
if value == "water":
# Override the auto-generated color for a specific class.
symbol = category.symbol().clone()
symbol.setColor(QColor("#2563eb"))
renderer.updateCategorySymbol(i, symbol)
renderer.updateCategoryLabel(i, "Open Water")
layer.triggerRepaint()
Breakdown: renderer.categories() returns the ordered list; the index i is what updateCategorySymbol() and updateCategoryLabel() expect. Cloning the symbol before mutating it avoids aliasing surprises where two categories share the same object. To control colors at the per-feature or per-symbol level rather than per-class, see Programmatic Layer Styling.
3. Graduating a Numeric Field into Classes
A graduated renderer maps a continuous field onto a small number of ordered classes. The workflow is: instantiate the renderer on the field, choose a classification method, set a class count and color ramp, then call updateClasses() to compute the break values.
from qgis.core import (
QgsGraduatedSymbolRenderer,
QgsClassificationQuantile,
QgsStyle,
QgsSymbol,
)
layer = QgsProject.instance().mapLayersByName("counties")[0]
field_name = "population"
renderer = QgsGraduatedSymbolRenderer(field_name)
renderer.setSourceSymbol(QgsSymbol.defaultSymbol(layer.geometryType()))
# Classification method drives where the class breaks fall.
renderer.setClassificationMethod(QgsClassificationQuantile())
# Compute 5 quantile classes from the layer's population values.
renderer.updateClasses(layer, 5)
# Apply a sequential ramp suited to ordered magnitude data.
ramp = QgsStyle.defaultStyle().colorRamp("Greens")
renderer.updateColorRamp(ramp)
layer.setRenderer(renderer)
layer.triggerRepaint()
Breakdown: setSourceSymbol() defines the base symbol that every class clones before recoloring. setClassificationMethod() plugs in the algorithm that decides break positions — swapping it changes the map dramatically. updateClasses(layer, 5) runs that method against the field and creates five QgsRendererRange objects. Because magnitude has a natural order, a sequential ramp (Greens, Blues, Viridis) is correct here, unlike the qualitative ramp used for categories.
4. Choosing a Classification Method
QGIS 3.x exposes classification as pluggable QgsClassificationMethod subclasses. The three you will reach for most are equal interval, quantile, and natural breaks. They produce different maps from identical data, so the choice is cartographic, not cosmetic.
from qgis.core import (
QgsClassificationEqualInterval,
QgsClassificationQuantile,
QgsClassificationJenks,
)
methods = {
"equal": QgsClassificationEqualInterval(),
"quantile": QgsClassificationQuantile(),
"jenks": QgsClassificationJenks(),
}
for name, method in methods.items():
renderer.setClassificationMethod(method)
renderer.updateClasses(layer, 5)
breaks = [f"{r.lowerValue():.0f}-{r.upperValue():.0f}"
for r in renderer.ranges()]
print(name, breaks)
Breakdown: Each method computes break points differently. QgsClassificationEqualInterval divides the value range into equal-width bins — fast and intuitive, but a few outliers can leave most classes empty. QgsClassificationQuantile puts an equal count of features in each class — great for ranked maps, but break values look arbitrary. QgsClassificationJenks (natural breaks) minimizes within-class variance and maximizes between-class variance, hugging natural clusters in the data. For a deep treatment of Jenks and when to prefer it, see Classify a Layer with Natural Breaks (Jenks) in PyQGIS.
| Method | Best for | Watch out for |
|---|---|---|
| Equal interval | Evenly spread data, intuitive legends | Skewed data leaves classes empty |
| Quantile | Ranked maps, even map ink balance | Adjacent similar values split across classes |
| Jenks (natural breaks) | Clustered, real-world distributions | Slower on very large layers; breaks vary with N |
Two further methods are worth knowing. QgsClassificationLogarithmic spaces breaks on a log scale, which tames heavily right-skewed data such as population or income where a handful of huge values would otherwise compress everyone else into the bottom class. QgsClassificationPrettyBreaks rounds break values to human-friendly numbers (10, 25, 50) at the cost of slightly uneven class membership — ideal when a clean legend matters more than statistical precision. All of them are interchangeable: every method is a QgsClassificationMethod subclass, so setClassificationMethod() accepts any of them and updateClasses() does the rest. That uniform interface is what makes it cheap to offer users a method dropdown in a plugin and apply their choice with one line.
5. Color Ramps and updateColorRamp()
Both renderers store classes (or categories) plus a color ramp, and updateColorRamp() recolors every class without recomputing the breaks. Ramps come from QgsStyle.defaultStyle(), which exposes every ramp in the user's QGIS style library by name.
from qgis.core import QgsStyle, QgsGradientColorRamp
from qgis.PyQt.QtGui import QColor
style = QgsStyle.defaultStyle()
# Discover available ramp names.
print(style.colorRampNames()[:10])
# Built-in ramp by name.
viridis = style.colorRamp("Viridis")
renderer.updateColorRamp(viridis)
# Or build a custom two-color gradient on the fly.
custom = QgsGradientColorRamp(QColor("#edf8e9"), QColor("#005a32"))
renderer.updateColorRamp(custom)
# Reverse a ramp so high values map to dark colors.
viridis_inv = style.colorRamp("Viridis")
viridis_inv.invert()
renderer.updateColorRamp(viridis_inv)
layer.triggerRepaint()
Breakdown: colorRampNames() lists everything available, including cpt-city ramps the user installed. updateColorRamp() is non-destructive to your class breaks — it only restyles, so you can preview several ramps quickly. QgsGradientColorRamp lets you bypass the library entirely when you need brand-specific colors. invert() flips the direction, which matters when convention expects darker symbols for larger magnitudes.
6. Reading and Reusing an Existing Renderer
Production scripts often inherit a layer that is already styled and need to inspect or extend that styling rather than rebuild it from scratch. Renderers are introspectable: you can read the type, the field, the breaks, and the symbols, then make targeted changes. Guarding on the renderer class keeps the same routine safe whether the layer arrives categorized, graduated, or with a plain single-symbol renderer.
from qgis.core import (
QgsGraduatedSymbolRenderer,
QgsCategorizedSymbolRenderer,
)
layer = QgsProject.instance().mapLayersByName("counties")[0]
renderer = layer.renderer()
if isinstance(renderer, QgsGraduatedSymbolRenderer):
print(f"Graduated on '{renderer.classAttribute()}' "
f"with {len(renderer.ranges())} classes")
# Clone so we never mutate the live renderer in place.
new_renderer = renderer.clone()
new_renderer.updateColorRamp(QgsStyle.defaultStyle().colorRamp("Magma"))
layer.setRenderer(new_renderer)
elif isinstance(renderer, QgsCategorizedSymbolRenderer):
print(f"Categorized on '{renderer.classAttribute()}' "
f"with {len(renderer.categories())} categories")
else:
print(f"Single-symbol or unsupported renderer: {type(renderer).__name__}")
layer.triggerRepaint()
Breakdown: layer.renderer() returns the live renderer object; isinstance() checks let you branch on its concrete type before calling type-specific methods such as ranges() or categories(). classAttribute() reports the field the renderer is bound to, which is invaluable in batch jobs where the field name is not known ahead of time. Calling clone() before mutating is the safe habit: it produces an independent copy so an exception mid-edit cannot leave the displayed renderer half-modified. This pattern is the backbone of any tool that restyles many layers consistently — for example, applying a house color ramp across an atlas.
7. Saving, Loading, and Sharing Styles
Once a renderer looks right, capture it so the same classification and colors apply to other layers or projects without re-running the classification logic. QGIS serializes styling to .qml (QML) files and can also embed it in the data source via a sidecar or database table.
# Export the full symbology (renderer + labels) to a QML file.
result, message = layer.saveNamedStyle("/tmp/counties.qml")
print("saved" if result else f"failed: {message}")
# Apply that style to a second, similarly-structured layer.
other = QgsProject.instance().mapLayersByName("counties_2020")[0]
ok, msg = other.loadNamedStyle("/tmp/counties.qml")
other.triggerRepaint()
# Export only the symbology category (omit labels, diagrams) if needed.
from qgis.core import QgsMapLayer
layer.saveNamedStyle(
"/tmp/counties_symbology.qml",
categories=QgsMapLayer.Symbology,
)
Breakdown: saveNamedStyle() returns a (bool, str) tuple — always check it, since a read-only path is a common silent failure. loadNamedStyle() applies a saved style to any compatible layer; the field referenced by the renderer must exist in the target or the classes render empty. The categories= argument lets you export a subset (symbology only, labels only, or all), which is useful when you want to share class colors without overwriting a layer's existing labels configured in Labeling & Annotations. For team workflows, commit the .qml alongside the data so every analyst renders the layer identically.
8. Refreshing the Legend and Canvas
A renderer change does not automatically propagate to the Layers panel legend until you tell QGIS to rebuild it. In scripts, refresh both the symbology cache and the layer tree view.
from qgis.utils import iface
layer.setRenderer(renderer)
# Rebuild the legend nodes for this layer in the tree.
layer.triggerRepaint()
if iface is not None:
iface.layerTreeView().refreshLayerSymbology(layer.id())
iface.mapCanvas().refresh()
Breakdown: triggerRepaint() invalidates the rendered image. refreshLayerSymbology() regenerates the expandable legend entries (the swatches under the layer name). iface.mapCanvas().refresh() forces an immediate redraw. The iface is not None guard keeps the same code safe in standalone scripts where there is no GUI. To save the styling for reuse, call layer.saveNamedStyle("/path/to/style.qml").
Version Compatibility
The classification-method API used here — setClassificationMethod() plus updateClasses() — has been stable since QGIS 3.10 and is the recommended path on the 3.34 LTR baseline.
| QGIS / Python | Notes |
|---|---|
| 3.34 LTR (Python 3.12) | Baseline for this cluster. All APIs above work as written. |
| 3.28 LTR (Python 3.9) | Fully supported. QgsClassificationJenks and friends available. |
| 3.40 / 3.44 | Same API; additional built-in ramps and refined Jenks performance. |
| Pre-3.10 | Avoid: older code used QgsGraduatedSymbolRenderer.createRenderer(...) with a mode enum (Jenks, Quantile) instead of method objects. |
If you maintain code targeting older releases, the legacy mode-enum constructor still exists but is deprecated; migrate to method objects for forward compatibility.
Key Takeaways
- Use
QgsCategorizedSymbolRendererfor nominal data (one symbol per value) andQgsGraduatedSymbolRendererfor numeric magnitude (ordered classes). - Build categories with
createCategories([], template, layer, field); build classes withsetClassificationMethod(...)thenupdateClasses(layer, n). - Pick the classification method deliberately: equal interval, quantile, and Jenks produce visibly different maps.
updateColorRamp()restyles classes without recomputing breaks — use qualitative ramps for categories, sequential ramps for graduated values.- Always
triggerRepaint()andrefreshLayerSymbology()so the canvas and legend reflect the new renderer.
Frequently Asked Questions
When should I use a categorized renderer instead of a graduated one? Use categorized for discrete, unordered values such as land-use class, administrative name, or zoning code. Use graduated when the field is numeric and the magnitude matters — population, income, elevation. If you graduate a code field, the legend will imply an order that does not exist.
Why is my map all one color after setting a graduated renderer?
Almost always you set the renderer but never called updateClasses(layer, n), so there are zero ranges, or the chosen field contains nulls/strings that the method skipped. Confirm renderer.ranges() is non-empty and the field is numeric.
How do I keep the same class breaks but change colors?
Call renderer.updateColorRamp(new_ramp). It recolors every existing class or category in place and leaves your computed break values untouched, which makes ramp previewing fast.
Can I assign symbol size instead of color to graduated classes?
Yes. Set renderer.setGraduatedMethod(QgsGraduatedSymbolRenderer.GraduatedSize) and provide a min/max size with setSymbolSizes() (point/line layers). Graduated color is the default; graduated size produces proportional-symbol maps.
Do renderer changes persist when I save the project?
Yes — renderers are serialized into the .qgz/.qgs project file. To reuse styling across projects, export it with layer.saveNamedStyle("style.qml") and load it later with layer.loadNamedStyle().