Use the Style Manager and Symbol Library in PyQGIS

QGIS keeps a database of reusable symbols, colour ramps, text formats and label settings — the thing behind Settings → Style Manager. Most people treat it as somewhere symbols happen to live. Treated deliberately, it is how an organisation ships one house style to forty analysts and how a plugin stops hard-coding colours in twelve places.

This recipe belongs to Programmatic Layer Styling in PyQGIS. It covers reading and writing the default style database, saving symbols and ramps with tags, searching by tag, building a distributable XML style file, and loading one at plugin start-up.

What the style database holdsThe default style database stores four kinds of entity: symbols, colour ramps, text formats and label settings. Each is addressable by name and can carry any number of tags. The whole database, or a tagged subset of it, can be written to an XML style file and imported on another machine.One database, four entity types, one portable fileQgsStyle.defaultStyle()symbolssymbolNames()colour rampscolorRampNames()text formatstextFormatNames()label settingslabelSettingsNames()tags"house style" · "hydrology" · "draft"exportXml()one file, version-controllableimportXml() on every machinetags are the only practical way to find anything once the database is large

Prerequisites

  • QGIS 3.34 LTR (bundled Python 3.12) or newer.
  • A symbol you want to save; anything from a stacked symbol to a plain fill will do.
  • Write access to the user profile directory if you intend to modify the default database, which is where it lives.

Read what is already there

The default style is a singleton, and enumerating it is the fastest way to learn what QGIS ships with.

from qgis.core import QgsStyle

style = QgsStyle.defaultStyle()
print(len(style.symbolNames()), "symbols")
print(len(style.colorRampNames()), "colour ramps")
print([n for n in style.colorRampNames() if "Blues" in n])

Breakdown: defaultStyle() returns the user's own database, merged with the ones QGIS ships, so the counts differ between installations. Every getter that takes a name returns None for a name that is not present rather than raising, which is why so much styling code fails silently — setColorRamp(style.colorRamp("Blues ")) with a stray space produces a black layer and no message. Filtering the name list, as above, is a two-second way to confirm the spelling before hard-coding it.

Save a symbol with tags

Adding an entity takes a name; making it findable later takes tags.

from qgis.core import QgsStyle, QgsFillSymbol

symbol = QgsFillSymbol.createSimple({
    "color": "#2a6f97", "outline_color": "#123f5a", "outline_width": "0.4",
})

style = QgsStyle.defaultStyle()
style.addSymbol("Water body — house", symbol.clone(), True)
style.tagSymbol(QgsStyle.SymbolEntity, "Water body — house", ["house style", "hydrology"])

Breakdown: The third argument to addSymbol() is update, and passing True writes the change to the database immediately rather than only into the in-memory copy — omit it and the symbol vanishes when QGIS closes. symbol.clone() matters because the database takes ownership: passing the live symbol and then continuing to use it leads to a symbol whose lifetime is managed twice, which crashes rather than misbehaving. tagSymbol() takes the entity type as its first argument because the same call handles ramps and text formats through QgsStyle.ColorrampEntity and friends.

Colour ramps follow the same pattern, and are worth saving whenever a project uses a non-default one:

from qgis.core import QgsGradientColorRamp
from qgis.PyQt.QtGui import QColor

ramp = QgsGradientColorRamp(QColor("#f7fbff"), QColor("#08306b"))
style.addColorRamp("House blues", ramp, True)
style.tagSymbol(QgsStyle.ColorrampEntity, "House blues", ["house style"])

Breakdown: A two-stop gradient is the simplest ramp; QgsGradientColorRamp also accepts intermediate stops through setStops(), and QgsColorBrewerColorRamp wraps the standard cartographic schemes if you would rather use one of those than invent a scale. Tagging the ramp with the same tag as the symbols is what makes the export below select the whole house style in one query.

Find things by tag

Once a database has a few hundred entries, names stop being a way to find anything.

names = style.symbolsWithTag(
    QgsStyle.SymbolEntity, style.tagId("house style")
)
for symbol_id in names:
    print(style.symbolName(symbol_id))

Breakdown: symbolsWithTag() takes a numeric tag id rather than the string, and tagId() resolves it — returning 0 for a tag that does not exist, which then matches nothing and looks like an empty result rather than a mistake. The function returns entity ids, so a second lookup converts them to names. It is more indirection than it should be, and wrapping it in a small helper is worth doing once per codebase.

A tag is a house styleSymbols for water, vegetation, buildings and roads, together with two colour ramps, all carry the tag house style. Querying that tag returns the whole set regardless of geometry type or entity type, and exporting the query result produces one distributable file.One query, one coherent set, one filethe databaseWater bodyWoodlandBuildingRoad casingSample symbol (untagged) — not selectedtag: "house style" on the top foursymbolsWithTag(...)four symbols and two rampsexportXml("house.xml")one file for the whole teamand for version control

A house style as code

The version of this that survives contact with a team keeps the style definition in a Python module rather than in anybody's profile directory, and installs it from there.

from qgis.core import QgsStyle, QgsFillSymbol, QgsLineSymbol, QgsGradientColorRamp
from qgis.PyQt.QtGui import QColor

PREFIX = "ACME"

FILLS = {
    "Water": {"color": "#2a6f97", "outline_color": "#123f5a", "outline_width": "0.4"},
    "Woodland": {"color": "#4b7f52", "outline_color": "#2f5335", "outline_width": "0.3"},
    "Building": {"color": "#8a7f6d", "outline_color": "#3f3a32", "outline_width": "0.25"},
}


def install(style=None, tag="ACME house style"):
    style = style or QgsStyle.defaultStyle()
    for name, props in FILLS.items():
        full = f"{PREFIX} · {name}"
        symbol = QgsFillSymbol.createSimple(props)
        style.addSymbol(full, symbol.clone(), True)
        style.tagSymbol(QgsStyle.SymbolEntity, full, [tag])
    ramp = QgsGradientColorRamp(QColor("#f7fbff"), QColor("#08306b"))
    style.addColorRamp(f"{PREFIX} · Blues", ramp, True)
    style.tagSymbol(QgsStyle.ColorrampEntity, f"{PREFIX} · Blues", [tag])
    return style

Breakdown: Keeping the colours in a plain dictionary rather than scattered through constructor calls means a brand change is one edit and a review is one diff. The prefix in every name is what makes a later uninstall or upgrade possible — deleting everything matching a prefix is safe in a way that deleting by tag is not, because a user may have added their own symbols to the same tag. Defaulting style to None and resolving inside lets the same function install into the user's library or into a private in-memory style, which is exactly the flexibility a plugin needs between "set up my workspace" and "load my own symbols quietly".

Calling install() from a plugin's initGui() is cheap enough to do on every start, since addSymbol() with an existing name replaces rather than duplicating. That idempotence is what lets the house style be updated by shipping a new plugin version rather than by asking forty people to import a file.

Export and import a house style

The database is SQLite in the user profile; the portable form is XML.

style.exportXml("/data/styles/house_style.xml")

# on another machine, or at plugin start-up
incoming = QgsStyle()
incoming.createMemoryDatabase()
if not incoming.importXml("/data/styles/house_style.xml"):
    raise RuntimeError("house style file could not be read")

Breakdown: exportXml() writes the whole database, not a tagged subset — to distribute only part of it, build a second QgsStyle in memory, add the tagged entities to it, and export that. createMemoryDatabase() is what makes a QgsStyle usable without touching the user's own file, which is the right approach in a plugin: read your symbols into a private style object rather than mutating the analyst's library behind their back.

Where the intent genuinely is to install into the user's library, QgsStyle.defaultStyle().importXml() merges the file in. Name collisions overwrite silently, so prefixing every entity with the organisation's name is worth the verbosity.

QGIS version compatibility

The examples target QGIS 3.34 LTR (Python 3.12).

QGIS versionPythonNotes
3.16 LTR3.7QgsStyle, tagging and XML import/export present.
3.22 LTR3.9Text formats and label settings storable alongside symbols.
3.28 LTR3.9createMemoryDatabase() for private in-plugin style objects.
3.34 LTR3.12Baseline for this page.
3.40+3.12Legend patch shapes and 3D symbols added as entity types.

Troubleshooting

  • The symbol disappeared after restarting QGIS. addSymbol() was called without update=True, so it never reached the database.
  • QGIS crashed after saving a symbol. The live symbol object was passed rather than a clone, so ownership is shared. Always pass symbol.clone().
  • colorRamp() returned None. The name does not match exactly. Filter colorRampNames() rather than trusting a remembered spelling.
  • symbolsWithTag() returns nothing. tagId() gave 0 for a tag that does not exist. Check style.tags() first.
  • Importing overwrote a colleague's symbols. Names collide silently on import. Prefix distributed entities.
  • The exported file is enormous. exportXml() writes the entire database including everything QGIS ships. Build a memory style with only your entities.

Conclusion

Treat the style database as a shared asset rather than a scratchpad: name entities predictably, tag them so they can be found, clone before handing symbols over, and pass update=True so changes persist. For distribution, build a private in-memory style and export that, which keeps the file small and leaves the user's own library alone.

Frequently Asked Questions

Where is the style database on disk?symbology-style.db in the active user profile directory, which QgsApplication.qgisSettingsDirPath() reports. It is SQLite, so it can be inspected directly — but write to it through the API rather than with SQL.

Can a project carry its own style database? A project stores the styles its layers use, but not a library. To ship a library with a project, distribute the XML alongside it and import at load time from a project-read hook.

How do I save a whole layer style rather than one symbol? That is a QML style rather than a library entry. The library holds reusable pieces; QML holds a complete layer style including renderer and labelling.

Can I store a colour ramp used by a heatmap? Yes — ramps are entity type ColorrampEntity regardless of what consumes them, so a ramp saved here is available to a heatmap renderer, a graduated renderer or a raster.