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.
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.
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 version | Python | Notes |
|---|---|---|
| 3.16 LTR | 3.7 | loadSldStyle / saveSldStyle present; SLD 1.1 output. |
| 3.22 LTR | 3.9 | Improved round-tripping of graduated renderer class breaks. |
| 3.28 LTR | 3.9 | Better handling of SLD 1.0 input from older GeoServer versions. |
| 3.34 LTR | 3.12 | Baseline for this page. |
| 3.40+ | 3.12 | Additional 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
Opacityelements 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.