PyQGIS Cartography & Data Visualization

Cartography is where geospatial analysis becomes communication. A correctly buffered, reprojected, and joined dataset still says nothing useful until it is rendered with deliberate symbology, classified into meaningful classes, and labeled clearly. PyQGIS exposes the entire QGIS rendering stack to Python, which means every styling decision a cartographer makes through the QGIS interface — fill color, stroke width, graduated breaks, label placement, raster color ramps — can be scripted, parameterized, and reproduced across hundreds of layers without touching a single dialog.

This pillar is for GIS developers and analysts who already load and process data in PyQGIS and now want to control how it looks. We work from the rendering model upward: first the objects QGIS uses to turn features into pixels, then single-symbol styling, thematic classification, labeling, raster visualization, color ramps, style persistence, and finally pushing styled layers into export-ready deliverables. Every example targets QGIS 3.34 LTR (Python 3.12) and notes where the API diverges on 3.28 LTR (Python 3.9) or the newer 3.40/3.44 line.

Why script cartography at all, when the QGIS symbology dialogs are excellent? Three reasons recur in production work. Consistency — a single function that styles every layer in a project guarantees identical breaks, ramps, and label rules across maps that must compare like-for-like. Scale — applying a style to one layer is a five-minute dialog session; applying it to two hundred monthly data drops is a loop. Reproducibility — a styling script is version-controllable, reviewable, and rerunnable, so a map produced today can be regenerated identically next year from updated data. Manual styling remains the right choice for exploratory, one-off design; everything repeatable belongs in code.

The PyQGIS rendering pipeline A data source feeds a renderer (single-symbol, categorized, or graduated), which selects symbols built from symbol layers and combines them with labeling settings; the result is drawn to the map canvas and exported to a map deliverable. Data source vector / raster Renderer Single symbol Categorized Graduated Raster / pseudocolor Symbol layers fill / line / marker color ramps stacked layers Labeling PalLayerSettings placement / buffer expressions Map canvas live preview Exported map PDF / PNG

What You'll Learn

This pillar is organized into three focused clusters, each drilling into a major area of programmatic cartography:

  • Programmatic Layer Styling — building and assigning QgsSymbol objects, manipulating symbol layers, setting fill, stroke, marker, and line properties, and applying styles to both vector and raster layers from code.
  • Graduated & Categorized Renderers — turning attribute values into thematic maps: choropleths, classification methods (equal interval, quantile, natural breaks/Jenks), and category-per-value symbology.
  • Labeling & Annotations — configuring QgsPalLayerSettings, expression-driven and rule-based labels, text buffers, placement engines, and annotation items.

By the end you should be able to take an unstyled layer loaded by your processing pipeline and emit a finished, classified, labeled, export-ready map entirely from a script.

The PyQGIS Rendering & Symbology Model

Before styling anything, it helps to understand the chain of objects QGIS walks every time it draws a layer. A QgsVectorLayer owns a renderer — an object that decides, for each feature, which symbol to use. The renderer is retrieved with layer.renderer() and replaced with layer.setRenderer(). The simplest renderer, QgsSingleSymbolRenderer, returns the same symbol for every feature; QgsCategorizedSymbolRenderer and QgsGraduatedSymbolRenderer pick a symbol based on an attribute.

A symbol (QgsSymbol, with the concrete subclasses QgsFillSymbol, QgsLineSymbol, and QgsMarkerSymbol) is itself a container. It holds an ordered stack of symbol layers (QgsSymbolLayer), and each symbol layer contributes one rendering pass. A single fill symbol might stack a QgsSimpleFillSymbolLayer underneath a QgsLinePatternFillSymbolLayer to produce a hatched polygon with a solid base. This layering is the source of QGIS's cartographic depth, and it is fully addressable from Python.

from qgis.core import QgsProject, QgsFillSymbol

# Grab the active polygon layer and inspect its rendering objects.
layer = QgsProject.instance().mapLayersByName("districts")[0]

renderer = layer.renderer()
print("Renderer type:", renderer.type())          # e.g. 'singleSymbol'
symbol = renderer.symbol()                         # the QgsFillSymbol in use
print("Symbol layer count:", symbol.symbolLayerCount())

for i in range(symbol.symbolLayerCount()):
    sl = symbol.symbolLayer(i)
    print(i, sl.layerType(), sl.color().name())

# Replace the whole symbol with a freshly built one.
new_symbol = QgsFillSymbol.createSimple({
    "color": "#cfe8e3",
    "outline_color": "#0f766e",
    "outline_width": "0.4",
})
renderer.setSymbol(new_symbol)
layer.triggerRepaint()                             # redraw on the canvas

Breakdown: We read the layer's existing renderer, walk its symbol layers to see what is drawn, then swap in a new QgsFillSymbol created from a property dictionary. createSimple() is the fastest way to build a one-layer symbol. triggerRepaint() is what actually refreshes the canvas — without it, the renderer changes but the display stays stale. On QGIS 3.28 the same code runs unchanged; the property keys for createSimple() have been stable across the 3.x line.

If you are new to retrieving layers and the QgsProject singleton, the PyQGIS Fundamentals & Environment Setup pillar covers the application context and project model these snippets assume.

Two properties of this model are worth internalizing because they shape every later section. First, the renderer is the only thing that decides feature symbology — there is no per-feature color stored on the layer itself, so to vary appearance you either swap renderers or attach data-defined properties to a symbol layer. Second, symbols are mutable but cloning matters: when you assign one QgsSymbol instance to several categories or layers, edits to it can leak across them, so call symbol.clone() whenever you reuse a symbol as a starting point. Getting these two facts right prevents the most common class of "my changes affected the wrong features" bugs.

It also helps to know the units system. Symbol sizes, line widths, and label sizes default to millimeters, but most properties accept a render-unit setter — setWidthUnit(QgsUnitTypes.RenderPixels) or map units for scale-dependent symbology. Mixing millimeters (constant on the printed page) with map units (constant on the ground) is a deliberate cartographic choice: roads that should stay a fixed visual width use millimeters, while buffers that represent a real-world distance use map units.

Styling Vector Layers Programmatically

Most styling work begins with single-symbol rendering: one consistent look applied to every feature in a layer. The pattern differs slightly by geometry type because each maps to a different symbol subclass — QgsFillSymbol for polygons, QgsLineSymbol for lines, and QgsMarkerSymbol for points. The key insight is that you rarely need to construct symbol layers by hand; createSimple() accepts a dictionary of the same properties exposed in the symbology dialog.

from qgis.core import (
    QgsProject,
    QgsFillSymbol,
    QgsLineSymbol,
    QgsMarkerSymbol,
    QgsSingleSymbolRenderer,
)


def apply_single_symbol(layer, props):
    """Assign a single-symbol renderer appropriate to the layer geometry."""
    geom_type = layer.geometryType()  # 0=point, 1=line, 2=polygon
    if geom_type == 2:
        symbol = QgsFillSymbol.createSimple(props)
    elif geom_type == 1:
        symbol = QgsLineSymbol.createSimple(props)
    else:
        symbol = QgsMarkerSymbol.createSimple(props)
    layer.setRenderer(QgsSingleSymbolRenderer(symbol))
    layer.triggerRepaint()


rivers = QgsProject.instance().mapLayersByName("rivers")[0]
apply_single_symbol(rivers, {
    "line_color": "#2563eb",
    "line_width": "0.6",
    "capstyle": "round",
})

stations = QgsProject.instance().mapLayersByName("stations")[0]
apply_single_symbol(stations, {
    "name": "circle",
    "color": "#b45309",
    "size": "3",
    "outline_color": "#17211d",
})

Breakdown: A single helper handles all three geometry types by branching on layer.geometryType(). Note that property keys are geometry-specific: lines use line_color/line_width, markers use name/size/color, and fills use color/outline_color. Building a fresh QgsSingleSymbolRenderer and assigning it with setRenderer() cleanly replaces whatever was there before.

For data-driven overrides — making stroke width respond to an attribute, for instance — you set a QgsProperty expression on a symbol layer using setDataDefinedProperty(). A symbol layer exposes a registry of overridable properties (QgsSymbolLayer.PropertyStrokeWidth, PropertyFillColor, PropertySize, and many more); binding an expression such as QgsProperty.fromExpression('"flow" / 50') to one of them lets a single symbol scale itself per feature without building a graduated renderer. This is the right tool when the visual mapping is continuous and formulaic rather than binned into discrete classes.

Stacking is the other half of expressive single-symbol styling. Because a symbol holds an ordered list of symbol layers, you can append a second pass — symbol.appendSymbolLayer(QgsSimpleLineSymbolLayer.create({...})) — to draw, say, a casing line beneath a road's center line, or a drop-shadow offset fill beneath a polygon. Order is bottom-up: index 0 draws first and sits visually lowest. The full toolkit of fills, line patterns, marker placement, casing, and data-defined symbology is the subject of the Programmatic Layer Styling cluster, including the common task of setting a vector layer's symbol color.

Thematic Maps with Categorized & Graduated Renderers

The leap from a styled layer to a map that says something is classification. Two renderers do the heavy lifting. QgsCategorizedSymbolRenderer assigns a distinct symbol to each unique value of a field — ideal for nominal data like land-use class or administrative type. QgsGraduatedSymbolRenderer bins a numeric field into ranges and ramps a symbol property across them — the engine behind every choropleth.

The graduated renderer needs three decisions: which field, how many classes, and which classification method. The method object (QgsClassificationEqualInterval, QgsClassificationQuantile, QgsClassificationJenks) computes the break points.

from qgis.core import (
    QgsProject,
    QgsGraduatedSymbolRenderer,
    QgsClassificationJenks,
    QgsStyle,
)

layer = QgsProject.instance().mapLayersByName("counties")[0]

renderer = QgsGraduatedSymbolRenderer()
renderer.setClassAttribute("pop_density")
renderer.setClassificationMethod(QgsClassificationJenks())

# Apply a built-in color ramp across 5 natural-breaks classes.
ramp = QgsStyle.defaultStyle().colorRamp("Reds")
renderer.updateClasses(layer, QgsGraduatedSymbolRenderer.Jenks, 5)
renderer.updateColorRamp(ramp)

layer.setRenderer(renderer)
layer.triggerRepaint()

Breakdown: We set the classification field and method, then call updateClasses() to compute the breaks from the layer's data and updateColorRamp() to color them. QgsStyle.defaultStyle().colorRamp("Reds") pulls a named ramp from the bundled style library. On QGIS 3.34 the setClassificationMethod() API is the recommended path; on 3.28 the older setMode(QgsGraduatedSymbolRenderer.Jenks) enum still works and is what updateClasses() accepts in both versions. The newer 3.40+ releases keep both but emphasize the method-object API.

The categorized renderer follows a similar rhythm but enumerates unique values instead of computing breaks. You typically build it by querying the layer's distinct values, creating one QgsRendererCategory per value with its own symbol, and assembling them into a QgsCategorizedSymbolRenderer(field, categories). A practical refinement is reserving an explicit catch-all category with an empty value string, which captures any feature whose attribute does not match a defined class — without it, unmatched features render invisibly and quietly disappear from the map.

Two cartographic cautions apply to both renderers. First, normalize before you classify: mapping raw counts (population) instead of rates (population per km²) produces choropleths that simply restate where the big polygons are. Compute the rate into a field, or feed an expression-based class attribute, before classifying. Second, keep class counts modest — five to seven classes is the readable maximum for most sequential ramps, because the human eye cannot reliably distinguish more shades of a single hue.

Choosing a classification method materially changes the story your map tells — quantile spreads features evenly across classes, while Jenks/natural breaks minimizes within-class variance and equal interval preserves honest value spacing at the cost of lopsided bins on skewed data. The trade-offs, plus full recipes for building choropleth maps and classifying a layer with natural breaks (Jenks), live in the Graduated & Categorized Renderers cluster.

Labeling & Annotations

A thematic map is incomplete without text. QGIS labeling is configured through QgsPalLayerSettings, which controls everything from the field or expression that supplies label text to placement, priority, and rendering. You wrap those settings in a QgsVectorLayerSimpleLabeling object and hand it to the layer, then flip labels on.

from qgis.core import (
    QgsProject,
    QgsPalLayerSettings,
    QgsTextFormat,
    QgsTextBufferSettings,
    QgsVectorLayerSimpleLabeling,
)
from qgis.PyQt.QtGui import QColor, QFont

layer = QgsProject.instance().mapLayersByName("cities")[0]

text_format = QgsTextFormat()
text_format.setFont(QFont("Noto Sans", 10))
text_format.setSize(10)

buffer = QgsTextBufferSettings()
buffer.setEnabled(True)
buffer.setSize(1.0)
buffer.setColor(QColor("#ffffff"))
text_format.setBuffer(buffer)

settings = QgsPalLayerSettings()
settings.fieldName = "\"name\" || ' (' || format_number(\"pop\") || ')'"
settings.isExpression = True
settings.setFormat(text_format)
settings.placement = QgsPalLayerSettings.OverPoint

layer.setLabeling(QgsVectorLayerSimpleLabeling(settings))
layer.setLabelsEnabled(True)
layer.triggerRepaint()

Breakdown: QgsTextFormat defines the font and a white halo (QgsTextBufferSettings) so labels stay legible over busy backgrounds. Setting fieldName to an expression and isExpression = True lets us concatenate the city name with a formatted population. placement chooses the engine — OverPoint here, with AroundPoint, Line, and Curved available for other geometries. setLabelsEnabled(True) is the switch that turns the layer's labels on.

Three settings separate amateur labels from professional ones. Placement priority and conflict resolution — QGIS's PAL engine drops labels that would overlap, so set settings.priority and mark important layers' labels as obstacles or non-removable to control which survive a crowded extent. Scale-based visibility, configured with settings.scaleVisibility, settings.minimumScale, and settings.maximumScale, keeps minor place names hidden until the user zooms in. And callout lines (settings.setCallout()) draw a leader from a displaced label back to its feature, which is essential when point density forces labels off their markers.

For polygons and lines, placement mode matters as much as text format: QgsPalLayerSettings.PerimeterCurved follows a river or coastline, while OffsetFromCentroid or Horizontal suit area features. Expression labels, rule-based label sets that change with scale, and annotation items (free-floating text and callouts) are covered in depth — including adding rule-based labels — in the Labeling & Annotations cluster.

Raster Styling

Rasters use a parallel but separate renderer hierarchy. For continuous single-band data — elevation, temperature, an index — the workhorse is QgsSingleBandPseudoColorRenderer, which maps pixel values to colors through a QgsColorRampShader. You define the shader's classes (value/color pairs), choose an interpolation mode, and attach it to the renderer.

from qgis.core import (
    QgsProject,
    QgsRasterShader,
    QgsColorRampShader,
    QgsSingleBandPseudoColorRenderer,
)
from qgis.PyQt.QtGui import QColor

dem = QgsProject.instance().mapLayersByName("elevation")[0]
provider = dem.dataProvider()
band = 1

stats = provider.bandStatistics(band)
vmin, vmax = stats.minimumValue, stats.maximumValue

color_shader = QgsColorRampShader(vmin, vmax)
color_shader.setColorRampType(QgsColorRampShader.Interpolated)
color_shader.setColorRampItemList([
    QgsColorRampShader.ColorRampItem(vmin, QColor("#0f766e"), "low"),
    QgsColorRampShader.ColorRampItem((vmin + vmax) / 2, QColor("#facc15"), "mid"),
    QgsColorRampShader.ColorRampItem(vmax, QColor("#b45309"), "high"),
])

shader = QgsRasterShader()
shader.setRasterShaderFunction(color_shader)

renderer = QgsSingleBandPseudoColorRenderer(provider, band, shader)
dem.setRenderer(renderer)
dem.triggerRepaint()

Breakdown: We pull real min/max from bandStatistics() so the ramp fits the data instead of guessing. The QgsColorRampShader is built with three interpolated stops; Interpolated blends between them, while Discrete and Exact give stepped or value-matched output. The renderer needs the data provider, band number, and the wrapping QgsRasterShader. For hillshade, you would instead use QgsHillshadeRenderer (or run processing.run("gdal:hillshade", ...)) and overlay it semi-transparently above the pseudocolor layer. Applying a color ramp to a raster is detailed in the styling cluster.

Beyond pseudocolor, three other raster renderers cover most needs. QgsMultiBandColorRenderer composes red, green, and blue bands into a true- or false-color image — the standard for satellite and aerial imagery, where you also set per-band QgsContrastEnhancement (typically a 2–98% cumulative cut) so the visible range fills the display. QgsPalettedRasterRenderer maps discrete integer classes (land cover codes) to named colors. And QgsSingleBandGrayRenderer produces a stretched grayscale view useful for single-band panchromatic data. Switching renderer is always the same gesture — build the renderer against the data provider and call setRenderer() — which keeps raster styling conceptually parallel to vector styling.

The DEM and band statistics used here typically come out of a processing chain — see the Spatial Data Processing & Automation pillar for producing and reprojecting the rasters you visualize.

Color Ramps & QgsStyle

Hard-coding hex values works for one map; reusing a consistent palette across a project does not. QgsStyle is QGIS's style database — the registry of named symbols, color ramps, text formats, and label settings. QgsStyle.defaultStyle() gives you the bundled library (ColorBrewer ramps, named symbols), and you can register your own ramps for reuse.

from qgis.core import QgsStyle, QgsGradientColorRamp, QgsGradientStop
from qgis.PyQt.QtGui import QColor

style = QgsStyle.defaultStyle()

# List available ramps, then build and register a custom one.
print("Bundled ramps:", style.colorRampNames()[:8])

brand_ramp = QgsGradientColorRamp(
    QColor("#f6f3ea"),   # start
    QColor("#0f766e"),   # end
    discrete=False,
    stops=[QgsGradientStop(0.5, QColor("#facc15"))],
)

if style.colorRamp("brand_teal"):
    style.removeColorRamp("brand_teal")
style.addColorRamp("brand_teal", brand_ramp)

# Retrieve it later by name from anywhere in the session.
ramp = style.colorRamp("brand_teal")
print("Mid color:", ramp.color(0.5).name())

Breakdown: colorRampNames() enumerates what is available; QgsGradientColorRamp builds a custom gradient with intermediate QgsGradientStops. Registering it under a name with addColorRamp() makes it retrievable by colorRamp("brand_teal") for the rest of the session, so a graduated renderer or shader can pull the exact same palette. To persist a ramp across sessions, save the style database with style.exportXml(path). The QgsGradientStop constructor signature has been stable since 3.x; on 3.28 the keyword form shown here works identically.

Saving & Reusing Styles

Styling logic you build in a script should be persistable. QGIS supports two interchange formats: QML (its native, full-fidelity style XML) and SLD (OGC Styled Layer Descriptor, portable to GeoServer and other OGC servers but lossy for advanced QGIS features). Every map layer exposes saveNamedStyle() / loadNamedStyle() for QML and saveSldStyle() / loadSldStyle() for SLD.

from qgis.core import QgsProject
from qgis.PyQt.QtXml import QDomDocument

source = QgsProject.instance().mapLayersByName("counties")[0]

# Persist the fully-classified style to disk as QML, and as portable SLD.
source.saveNamedStyle("/data/styles/counties.qml")
source.saveSldStyle("/data/styles/counties.sld")

# Copy a style between layers in memory, with no file round-trip.
target = QgsProject.instance().mapLayersByName("counties_2020")[0]

doc = QDomDocument()
source.exportNamedStyle(doc)        # serialize source renderer + labeling
target.importNamedStyle(doc)        # apply it to the second layer
target.triggerRepaint()

Breakdown: saveNamedStyle() writes a .qml that captures the renderer, labeling, and ramps exactly; saveSldStyle() emits OGC SLD for server-side reuse. To clone a style onto another layer without writing a file, serialize the source into a QDomDocument with exportNamedStyle() and read it back with importNamedStyle() — the most reliable in-memory copy. A separate concept worth knowing is layer.styleManager(), which manages multiple named styles attached to one layer rather than copying the active style between layers. Prefer QML when staying inside QGIS; reach for SLD only when interoperating with non-QGIS OGC software.

A common automation pattern is to build one perfectly-styled template layer, save its QML once, then loadNamedStyle() it onto every freshly-processed layer in a batch — see Batch Processing with PyQGIS for the iteration scaffolding.

From Canvas to Deliverable

Styling pays off when it ships. Two output paths exist. For a quick image of the live canvas, QgsMapRendererParallelJob renders the current map to a QImage. For publication-quality output with legends, scale bars, and titles, you build a QgsPrintLayout, add a QgsLayoutItemMap, and export with QgsLayoutExporter.

from qgis.core import (
    QgsProject,
    QgsPrintLayout,
    QgsLayoutItemMap,
    QgsLayoutExporter,
    QgsLayoutPoint,
    QgsLayoutSize,
    QgsUnitTypes,
)

project = QgsProject.instance()
layout = QgsPrintLayout(project)
layout.initializeDefaults()      # A4 landscape page
layout.setName("auto_export")

map_item = QgsLayoutItemMap(layout)
map_item.attemptMove(QgsLayoutPoint(10, 10, QgsUnitTypes.LayoutMillimeters))
map_item.attemptResize(QgsLayoutSize(277, 190, QgsUnitTypes.LayoutMillimeters))
map_item.setExtent(project.mapLayersByName("counties")[0].extent())
layout.addLayoutItem(map_item)

exporter = QgsLayoutExporter(layout)
exporter.exportToPdf("/data/out/counties_map.pdf",
                     QgsLayoutExporter.PdfExportSettings())

Breakdown: initializeDefaults() gives a standard page; QgsLayoutItemMap is the window onto your styled layers, positioned and sized in millimeters and zoomed via setExtent(). QgsLayoutExporter.exportToPdf() flattens the layout — every renderer, label, and ramp you configured — into a PDF. Because the map item draws whatever symbology the layers currently carry, all the styling earlier in this pillar flows straight through to the deliverable.

A few export details determine whether the result is publication-grade. Set DPI explicitly on the PdfExportSettings (300 for print, 96 for screen) and enable rasterizeWholeImage = False so vector symbology stays crisp and selectable in the PDF. For raster image output, exportToImage() takes a QgsLayoutExporter.ImageExportSettings where you fix the pixel dimensions or DPI. When a layout contains a legend (QgsLayoutItemLegend), call legend.setAutoUpdateModel(True) so it reflects the current renderer classes — otherwise a graduated legend can drift out of sync with the breaks you computed at runtime. Because the exporter renders the same renderer, symbol, and label objects you configured earlier, the discipline of styling correctly upstream is what makes a hands-off, repeatable export pipeline possible.

For multi-map and multi-page production — atlases driven by a coverage layer, batch PDF exports — the Automated Map Layout Generation cluster in the processing pillar extends exactly this pattern.

Key Takeaways

  • Rendering is a chain: layer → renderer → symbol → symbol layers, with labeling configured separately and merged at draw time. Master renderer()/setRenderer() and you control the whole pipeline.
  • Use createSimple() with property dictionaries for fast single-symbol styling; branch on geometryType() to pick the right QgsFillSymbol/QgsLineSymbol/QgsMarkerSymbol.
  • Thematic maps come from QgsCategorizedSymbolRenderer (nominal) and QgsGraduatedSymbolRenderer (numeric); the classification method (equal interval, quantile, Jenks) shapes the story.
  • Labels live in QgsPalLayerSettings wrapped by QgsVectorLayerSimpleLabeling; always add a text buffer for legibility.
  • Raster visualization uses QgsSingleBandPseudoColorRenderer + QgsColorRampShader, fitted to real bandStatistics().
  • Centralize palettes in QgsStyle, persist styles as .qml (native) or .sld (portable), and copy them between layers via exportNamedStyle()/importNamedStyle().
  • Always call triggerRepaint() after changing symbology, and feed styled layers into QgsLayoutExporter for export-ready maps.

Frequently Asked Questions

Why don't my styling changes show up after I set a new renderer? The renderer is updated in memory, but the canvas caches the last render. Call layer.triggerRepaint() after setRenderer(), setLabeling(), or any symbol change. If the layer is in a layout map item, the layout picks up the change on its next refresh or export automatically.

What is the difference between a symbol and a symbol layer? A QgsSymbol is the complete visual for one feature class; a QgsSymbolLayer is one drawing pass inside it. Stacking symbol layers (for example a solid fill beneath a hatch) is how QGIS builds complex cartography. symbol.symbolLayerCount() and symbol.symbolLayer(i) let you inspect and edit each pass independently.

Should I save styles as QML or SLD? Use QML when the style stays within QGIS — it captures every feature, including data-defined overrides, blend modes, and rule-based labeling, with full fidelity. Use SLD only when you must share styling with OGC servers like GeoServer or MapServer; SLD cannot represent some QGIS-specific symbology and will silently drop it.

Which classification method should I use for a choropleth? For most thematic maps, natural breaks (Jenks) groups similar values and exposes real clusters in the data. Quantile guarantees equal feature counts per class, which is good for ranked comparisons but can mask outliers. Equal interval is honest about absolute value ranges but produces lopsided classes on skewed data. The Graduated & Categorized Renderers cluster compares them with worked examples.

Can I style layers in a headless script with no GUI? Yes. All the renderer, symbol, and labeling classes live in qgis.core and work under a standalone QgsApplication with no canvas. You can classify, label, and export to PDF entirely headless via QgsLayoutExporter — useful in CI and scheduled jobs. See the PyQGIS Fundamentals & Environment Setup pillar for initializing QGIS outside the desktop app.