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.
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 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 version | Python | Notes |
|---|---|---|
| 3.16 LTR | 3.7 | QgsStyle, tagging and XML import/export present. |
| 3.22 LTR | 3.9 | Text formats and label settings storable alongside symbols. |
| 3.28 LTR | 3.9 | createMemoryDatabase() for private in-plugin style objects. |
| 3.34 LTR | 3.12 | Baseline for this page. |
| 3.40+ | 3.12 | Legend patch shapes and 3D symbols added as entity types. |
Troubleshooting
- The symbol disappeared after restarting QGIS.
addSymbol()was called withoutupdate=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. FiltercolorRampNames()rather than trusting a remembered spelling.symbolsWithTag()returns nothing.tagId()gave0for a tag that does not exist. Checkstyle.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.