Apply an SLD Style to a Layer in PyQGIS

SLD — the OGC Styled Layer Descriptor — is the interchange format for map styling. GeoServer speaks it, MapServer can consume it, and QGIS reads and writes it, which makes it the only realistic way to move a style between a desktop project and a web map server. It is also lossy in one direction and quietly so, which is the part worth understanding before you rely on it.

This recipe belongs to Programmatic Layer Styling in PyQGIS. It covers loading and saving SLD from Python, checking the result rather than trusting it, what QGIS features have no SLD equivalent, and how to decide between SLD and QML for a given job.

What survives an SLD exportRenderer type, class breaks, colours and simple symbols are represented in SLD and reach other software intact. Stacked symbol layers, geometry generators, blend modes, and most data defined overrides have no SLD equivalent and are approximated or dropped. QML preserves all of it but is only readable by QGIS.Portable and lossy, or complete and QGIS-onlyQGIS stylerenderer + classessymbol layer stackgeometry generatorblend modedata-defined sizelabellingscale rangesSLD keepsrenderer, class breaks, colourssimple symbols, scale rangesbasic labellingSLD dropsstacks, generators, blendsmost data-defined overrideswhere it landsGeoServer · MapServeranother QGIS installalways open the resultand look at it

Prerequisites

  • QGIS 3.34 LTR (bundled Python 3.12) or newer.
  • A styled vector layer to export, or an SLD file to apply.
  • If the SLD came from GeoServer, know which SLD version it declares — 1.0 and 1.1 differ in several element names and QGIS is stricter about 1.1.

Apply an SLD file

One call, and a return value that must be checked.

from qgis.core import QgsProject

layer = QgsProject.instance().mapLayersByName("landuse")[0]
message, ok = layer.loadSldStyle("/data/styles/landuse.sld")

if not ok:
    raise RuntimeError(f"SLD not applied: {message}")

layer.triggerRepaint()

Breakdown: loadSldStyle() returns a (message, ok) tuple in that order — the reverse of loadNamedStyle(), which returns (message, ok) too but is frequently remembered the other way round. Ignoring the flag is the usual reason a script reports success while the layer still shows its default symbol: an SLD that references a symbolizer QGIS cannot build fails partially, applies what it could, and says so only in the message. Repainting is needed because loading a style does not itself invalidate the canvas.

The message is worth logging even on success, because a partial application produces a warning rather than a failure:

if message:
    QgsMessageLog.logMessage(f"SLD warnings for {layer.name()}: {message}", "Styling")

Breakdown: Sending it to the QGIS message log rather than printing keeps it visible in a plugin context where there is no console. In a batch that styles fifty layers, that log is the only record of which three came out approximately right.

Export a style as SLD

Writing is the same shape, with one extra decision.

message, ok = layer.saveSldStyle("/data/styles/landuse_export.sld")
if not ok:
    raise RuntimeError(f"SLD not written: {message}")

Breakdown: saveSldStyle() writes SLD 1.1 with QGIS's own vendor extensions where the standard has no equivalent. Those extensions are ignored by other software, which is the intended behaviour — a GeoServer reading the file gets a valid style that approximates yours rather than an error. What it does not do is warn you which parts became approximations, so the only reliable check is to read the file back into a fresh layer and compare.

from qgis.core import QgsVectorLayer

check = QgsVectorLayer(layer.source(), "check", layer.providerType())
check.loadSldStyle("/data/styles/landuse_export.sld")

original = layer.renderer()
roundtripped = check.renderer()
print(type(original).__name__, "→", type(roundtripped).__name__)

Breakdown: Comparing renderer classes catches the coarsest loss — a rule-based renderer arriving back as a categorized one, or as a single symbol. Comparing symbol layer counts per class catches the next tier. Neither is a full diff, but between them they identify the styles that need a hand-written SLD rather than an export.

What has no SLD equivalent

Four QGIS features account for most of the loss, and knowing them lets you design a style that survives.

Stacked symbol layers map onto SLD's multiple symbolizers reasonably well for two layers and unreliably beyond that; a four-layer road symbol usually returns as one or two. Geometry generators have no equivalent at all and are dropped silently. Blend modes are not in the standard, so a multiplied fill comes back as a normal one. Data-defined overrides map only where the expression is simple enough to become an SLD filter function — an attribute reference usually survives, arithmetic sometimes does, and anything using a QGIS-specific function does not.

Labelling is a partial case: font, size, colour and placement carry across, while callouts, per-part placement and most obstacle settings do not. Where a web service must reproduce complex labelling, it is generally quicker to write the SLD by hand for that layer than to iterate on an export.

Choosing between SLD and QMLIf the style only ever has to be read by QGIS, QML preserves everything and is the right choice. If another product must read it, SLD is the only option, and the style should then be built from features SLD can represent rather than exported and hoped for.Decide the destination before designing the stylewho reads this style?decide first, not afterQGIS onlyuse QML — nothing is loststacks, generators, blendsall survive intacta web service toouse SLD — and build the stylefrom what SLD can expressrather than exporting and hoping

Building a style SLD can carry

When the destination is a web service, the productive order is to design within the format rather than to export and repair. Four habits cover most of it.

Use one symbol layer per class. A graduated renderer whose classes each carry a single simple fill translates exactly; the moment a class gains a second symbol layer the translation becomes a guess. Put variation in the class breaks rather than in expressions — SLD filters can compare an attribute to a literal cleanly, and struggle with anything more elaborate. Keep sizes and widths static, since a data-defined size usually becomes a fixed one. And set scale ranges on the renderer's rules rather than on the layer, because rule-level ranges become SLD MinScaleDenominator elements while layer-level ones sometimes do not survive.

from qgis.core import (
    QgsGraduatedSymbolRenderer, QgsSymbol, QgsRendererRange, QgsFillSymbol,
)

renderer = QgsGraduatedSymbolRenderer("density")
renderer.setClassAttribute("density")

breaks = [(0, 20, "#edf8e9"), (20, 50, "#a1d99b"), (50, 200, "#31a354")]
for lower, upper, colour in breaks:
    symbol = QgsFillSymbol.createSimple({
        "color": colour, "outline_color": "#31302c", "outline_width": "0.2",
    })
    renderer.addClassRange(
        QgsRendererRange(lower, upper, symbol, f"{lower}{upper}")
    )

layer.setRenderer(renderer)
message, ok = layer.saveSldStyle("/data/styles/density.sld")

Breakdown: createSimple() with a plain dictionary produces exactly one symbol layer, which is the shape SLD represents faithfully. Giving each range an explicit label matters more than it looks: SLD carries the label into the service's legend, and an unlabelled range shows as a raw numeric expression to the end user. Building the renderer this way takes no longer than building an elaborate one and removes the round-trip surprise entirely — a style designed for the format needs no verification pass beyond opening it once.

Batch: style a folder from one SLD

Applying one style across many layers is where scripting pays for itself.

from pathlib import Path
from qgis.core import QgsVectorLayer, QgsProject

sld = "/data/styles/parcels.sld"
project = QgsProject.instance()

for path in sorted(Path("/data/parcels").glob("*.gpkg")):
    layer = QgsVectorLayer(str(path), path.stem, "ogr")
    if not layer.isValid():
        print(f"skipped {path.name}: failed to load")
        continue
    message, ok = layer.loadSldStyle(sld)
    if not ok:
        print(f"skipped {path.name}: {message}")
        continue
    project.addMapLayer(layer)

Breakdown: Continuing rather than raising on a failed layer is the right default in a batch — one corrupt file should not abandon the other forty — but the printed reason matters, because a silent continue produces a project that is quietly missing layers. Checking isValid() separately from the style load distinguishes a data problem from a styling one, which is worth the extra branch when the two have completely different fixes.

QGIS version compatibility

The examples target QGIS 3.34 LTR (Python 3.12).

QGIS versionPythonNotes
3.16 LTR3.7loadSldStyle / saveSldStyle present; SLD 1.1 output.
3.22 LTR3.9Improved round-tripping of graduated renderer class breaks.
3.28 LTR3.9Better handling of SLD 1.0 input from older GeoServer versions.
3.34 LTR3.12Baseline for this page.
3.40+3.12Additional labelling properties mapped to SLD text symbolizers.

Troubleshooting

  • The layer looks unchanged after loading. The return flag was not checked and the load failed. Print the message.
  • The style applied but looks simpler. Features with no SLD equivalent were dropped. Round-trip and compare renderer types.
  • GeoServer rejects the exported file. It contains QGIS vendor extensions that a strict parser refuses. Ask GeoServer for a validation report and remove the offending elements.
  • An SLD 1.0 file loads with warnings. Element names differ from 1.1. Converting with a tool, or asking the source to export 1.1, is quicker than patching by hand.
  • Colours are slightly off. The SLD carries opacity separately from colour. Check the Opacity elements as well as the fills.
  • Labels vanished. Text symbolizers were present but referenced a font that is not installed. Check the font before blaming the format.

Conclusion

Check the boolean from both loadSldStyle() and saveSldStyle(), log the message either way, and round-trip anything that matters before shipping it. Where a style must reach a web service, design it within what SLD can express rather than exporting a QGIS-specific design and discovering the gaps at the far end.

Frequently Asked Questions

Can I apply an SLD to a raster layer? Yes, and the raster symbolizer covers colour maps and contrast enhancement reasonably well. Complex renderers such as hillshade have no equivalent — see applying a colour ramp to a raster for the QGIS-native route.

Should I use SLD or QML for sharing between QGIS users? QML, always. It preserves everything, and it is the format QGIS itself uses when you save and load a named style.

Can I store an SLD in a GeoPackage? QGIS stores QML in a GeoPackage's style table by default. It can hold an SLD alongside it in the same table, which some other software will read.

How do I edit an SLD by hand? It is XML, so any editor works. Validate against the OGC schema afterwards, and remember that element order is significant in several places — a valid-looking file with symbolizers in the wrong order renders nothing.