Store Plugin Settings with QgsSettings
QgsSettings looks like a dictionary that survives restarts, and treating it as one works right up until the day a user reports that your checkbox will not stay off. The cause is almost always the same: values come back from the settings backend as strings on some platforms, the string "false" is truthy in Python, and the bug never reproduces on the machine that wrote the value.
This recipe belongs to Plugin Settings and Localization. It covers the key conventions that keep your plugin out of everybody else's namespace, typed reads with defaults, groups, storing structured values, reading QGIS's own configuration, and removing your keys cleanly.
Prerequisites
- QGIS 3.34 LTR (bundled Python 3.12) or newer.
- A plugin, or the Python console —
QgsSettingsworks anywhere. - A settled decision about what belongs in settings at all; the scope question is in Plugin Settings and Localization.
Write and read with types
from qgis.core import QgsSettings
PREFIX = "parcel_tools"
settings = QgsSettings()
settings.setValue(f"{PREFIX}/output_folder", "/data/exports")
settings.setValue(f"{PREFIX}/add_to_map", True)
settings.setValue(f"{PREFIX}/buffer_distance", 25.0)
settings.setValue(f"{PREFIX}/max_features", 5000)
folder = settings.value(f"{PREFIX}/output_folder", "", type=str)
add_to_map = settings.value(f"{PREFIX}/add_to_map", True, type=bool)
distance = settings.value(f"{PREFIX}/buffer_distance", 10.0, type=float)
limit = settings.value(f"{PREFIX}/max_features", 1000, type=int)
Breakdown: A module-level prefix constant means the plugin's namespace is written once and never mistyped. Every read passes two things: a default that is used when the key is absent, and a type= that forces the conversion. The type argument is the important one — without it, a boolean written on Windows and read back on Linux, or read after a settings file round-trip, can arrive as the string "false", and if settings.value(key): is then always true. The failure is invisible in testing because the value in memory during the same session is still a real boolean.
Deciding whether a key exists at all is a separate question from its value:
if not settings.contains(f"{PREFIX}/output_folder"):
settings.setValue(f"{PREFIX}/output_folder", default_export_folder())
Breakdown: contains() distinguishes "never configured" from "deliberately set to empty", which matters when an empty value is a legitimate choice. This is also the correct place to seed first-run defaults — doing it once on first run means the user can later clear a value without your code helpfully putting it back.
Group related keys
settings = QgsSettings()
settings.beginGroup(PREFIX)
try:
settings.setValue("output_folder", "/data/exports")
settings.setValue("add_to_map", True)
stored_keys = settings.allKeys()
finally:
settings.endGroup()
print(stored_keys) # ['add_to_map', 'output_folder']
Breakdown: Inside a group, keys are relative, which keeps long paths readable and makes it obvious that everything in the block belongs together. The try/finally matters more than it looks: an exception between beginGroup() and endGroup() leaves the settings object with a dangling group prefix, and every later read in that object silently addresses the wrong place. allKeys() inside the group lists what you have stored, which is genuinely useful for an options page that needs to enumerate saved profiles.
Store lists and structured values
Simple lists round-trip directly. Anything with structure is safer as JSON.
import json
settings.setValue(f"{PREFIX}/recent_folders", ["/data/a", "/data/b"])
recent = settings.value(f"{PREFIX}/recent_folders", [], type=list)
profile = {"crs": "EPSG:27700", "buffer": 25.0, "fields": ["ref", "area"]}
settings.setValue(f"{PREFIX}/profile", json.dumps(profile))
loaded = json.loads(settings.value(f"{PREFIX}/profile", "{}", type=str))
Breakdown: A list of strings survives the round trip on every platform; a list containing mixed types or nested structures does not, because the backends flatten differently. Serialising to JSON makes the storage format explicit, keeps types intact, and — usefully — makes the stored value human-readable if somebody inspects the settings file. Wrapping json.loads() in a try is worth it in shipped code: a truncated write, or a value edited by hand, otherwise raises somewhere unhelpful during startup.
Read QGIS's own settings
Your plugin is not the only thing that has been configured. Honouring QGIS's existing preferences saves the user answering the same question twice.
settings = QgsSettings()
default_crs = settings.value("app/projections/defaultProjectCrs", "EPSG:4326", type=str)
locale = settings.value("locale/userLocale", "en", type=str)
timeout_ms = settings.value("qgis/networkAndProxy/networkTimeout", 60000, type=int)
last_project_dir = settings.value("UI/lastProjectDir", "", type=str)
Breakdown: These are QGIS's own keys, readable by anybody. Using UI/lastProjectDir as the starting folder for your file dialog is a small touch that makes a plugin feel native, because it opens where the user last worked rather than in their home directory. The network timeout is worth respecting in any plugin that fetches something. Read these keys, do not write them: changing another component's configuration behind the user's back is how plugins get uninstalled.
Clean up on uninstall
def remove_all_settings():
settings = QgsSettings()
settings.remove(PREFIX) # removes the whole subtree
Breakdown: remove() on a group deletes it and everything beneath it in one call — the payoff for having namespaced properly in the first place. QGIS does not call anything on your plugin when it is uninstalled, so this belongs on a visible "reset settings" action in your options page rather than in unload(), which runs on every ordinary reload and would wipe preferences constantly. A reset action is also what you will ask a user to press when diagnosing a problem that turns out to be a stale value.
Decide what deserves to be a setting at all
The technical side of storing values is easy; the judgement about which values to store is where plugins go wrong in both directions.
A value earns a setting when the right answer genuinely differs between users and does not change between runs — an organisation's server address, a preferred output format, whether results are added to the map automatically. Those are real preferences: asked once, answered once, never thought about again.
A value does not deserve a setting when your plugin could work it out. A default output folder can come from the project folder; a default coordinate system can come from the project's; a default field can come from the layer's first suitable one. Every option you add is a question put to every user of the plugin, forever, and a plugin with thirty settings is usually one that could not decide anything for itself.
Two intermediate cases are worth naming. Values that change per run — the input layer, the output name, the buffer distance for this particular job — belong in the dialog, with the last value remembered as a convenience rather than presented as a preference. Values that belong to the map — which layer is authoritative, what parameters produced the current output — belong in the project, so a colleague opening it gets them too.
The test that resolves nearly every case is to ask what should happen when the user opens a different project tomorrow on a different machine. A preference should follow the person. A project fact should follow the file. Anything that should follow neither was working state, and should not have been persisted at all.
QGIS version compatibility
| QGIS version | Python | Notes |
|---|---|---|
| 3.22 LTR | 3.9 | QgsSettings with groups, typed reads and remove() as described. |
| 3.28 LTR | 3.9 | Identical. |
| 3.34 LTR | 3.12 | Baseline for this page. |
| 3.40 / 3.44 | 3.12 | Adds the typed settings-entry classes (QgsSettingsEntryString and friends), which declare a key, its type and default once and are worth adopting in new code. |
QSettings from PyQt still works, but bypasses QGIS's defaults and its profile handling — always prefer QgsSettings, which is a drop-in replacement.
Troubleshooting
- A boolean is always true. The value came back as a string. Add
type=boolto the read. - A setting resets every restart. It was written to a
QgsSettingsobject inside a group that was never closed, or the plugin writes a default over it on startup. Usecontains()before seeding defaults. - Values leak between plugins. Keys are not namespaced. Prefix every key with your plugin's folder name.
- A list comes back as a string. It contained non-string items. Store it as JSON instead.
- Settings vanish after switching profiles. They are per profile by design. That is also the easiest way to test a clean first run: launch with
--profile test. - Nothing persists in a headless script. Settings are written on destruction or on
sync(). Callsettings.sync()before a standalone script exits.
Conclusion
Namespace every key under your plugin's folder name, always read with both a default and a type=, group related keys and close the group in a finally, and serialise anything structured to JSON. Read QGIS's own settings to inherit sensible defaults, keep secrets in the authentication database instead, and offer a reset action that removes your whole subtree in one call.
Frequently Asked Questions
Where are the values actually stored? In the platform's standard location — the registry on Windows, an INI file in the active user profile elsewhere. Treat it as opaque; the file layout is not an API.
Can I use QSettings instead?
It works, but it misses QGIS's defaults and profile awareness. QgsSettings has the same interface, so there is no reason to.
How do I store a password? Do not. Create an authentication configuration and store its id, which is what Connect to a PostGIS Database in PyQGIS uses for exactly this reason.
Are settings shared between QGIS profiles? No. Each profile has its own settings, plugins and authentication database, which is what makes profiles useful for testing and for separating work contexts.
Should the plugin write settings on every change or on close? Write on change for anything the user would be annoyed to lose in a crash, and on close for high-frequency values such as window geometry.