Plugin Settings and Configuration Management in QGIS

Where plugin configuration belongs and how to store it: QgsSettings namespacing and typed reads, project-scoped entries, an options dialog page, credential…

Every plugin beyond a single button accumulates configuration: a server URL, a default output folder, a tolerance the user tuned once and never wants to type again. Where that configuration lives decides whether it survives a restart, whether it travels with a project to a colleague, and whether a credential ends up in a plain-text file. This page, part of the Plugin Development & UI Integration guide, covers QgsSettings and its namespacing rules, project-scoped values, the options panel that puts settings where users expect to find them, and the migration problem every second release runs into.

The organising question throughout is simple: does this value belong to the user, to the project, or to the machine? Each answer has a different store, and almost every configuration bug in the wild is a value filed under the wrong one.

Prerequisites Checklist

  • QGIS 3.28 LTR or newer. QgsSettings and QgsOptionsWidgetFactory are stable across 3.x.
  • A plugin skeleton with working initGui() and unload() methods, as covered in plugin lifecycle and resource management.
  • Python 3.9+ for the type hints used below.
  • A second QGIS profile to test with. Settings are per profile, and a fresh profile is the only honest way to see what a first-time user experiences.
  • Somewhere to put credentials. If your plugin talks to an authenticated service, read the authentication section below before writing any code that stores a password.

How Settings Storage Works

QgsSettings is a thin wrapper over Qt’s QSettings, pointed at the current QGIS user profile. It reads and writes the same store QGIS itself uses — an INI file on Linux and macOS, the registry on Windows — which has two consequences worth internalising.

What sits behind QgsSettings Three bands showing the settings stack: your namespaced keys, the QgsSettings wrapper that adds the QGIS section and type conversion, and the QSettings backend that writes an INI file or the platform registry. Your plugin plugin-prefixed keys myplugin/threshold explicit defaults and explicit types QgsSettings adds the QGIS section per user profile type conversion value(key, default, type=) Storage INI or registry per platform shared with QGIS same file The bottom band is shared with QGIS itself, which is why an unprefixed key is not merely untidy — it can collide with an application setting.

First, keys are a flat global namespace shared with the application. A key called threshold is not private to your plugin; it is a key in the QGIS settings tree that anything can read or overwrite. Every key your plugin writes must be prefixed with a plugin-specific section.

Second, values come back typed by the platform’s best guess unless you tell it otherwise. On Windows a stored True reliably returns as the string "true", which is truthy in Python and therefore compares equal to nothing you expect. Always pass an explicit type= argument when reading.

python
from qgis.core import QgsSettings

SECTION = "myplugin"          # every key this plugin owns lives under this prefix


def read_bool(key: str, default: bool) -> bool:
    """Read a boolean setting, immune to the platform's string coercion."""
    return QgsSettings().value("%s/%s" % (SECTION, key), default, type=bool)

Step-by-Step Implementation

Step 1 — Declare the defaults in one place

Scattering defaults through the code guarantees that two call sites will eventually disagree about what the default is. Declare them once, as a module-level mapping, and read through a helper that consults it.

python
from qgis.core import QgsSettings

SECTION = "myplugin"
DEFAULTS: dict[str, object] = {
    "buffer_distance": 25.0,
    "output_folder": "",
    "simplify_output": True,
    "service_url": "https://example.org/api",
}


def get(key: str):
    """Read `key` from the plugin's settings section, falling back to its default."""
    if key not in DEFAULTS:
        raise KeyError("unknown setting %r" % key)
    default = DEFAULTS[key]
    return QgsSettings().value("%s/%s" % (SECTION, key), default, type=type(default))


def put(key: str, value) -> None:
    """Write `key` immediately, so the value survives an unclean shutdown."""
    if key not in DEFAULTS:
        raise KeyError("unknown setting %r" % key)
    QgsSettings().setValue("%s/%s" % (SECTION, key), value)

The KeyError on an unknown key is deliberate. A typo in a settings key is otherwise silent: the read returns the default forever, and the write creates an orphan key nothing ever reads.

Step 2 — Write on change, not at shutdown

Persisting settings in unload() looks tidy and fails in exactly the case where the user most wants their configuration back. If QGIS crashes, unload() never runs. Write each value as it changes; the cost is a single INI write, and the store is designed for it.

When each setting is read and written A timeline of a plugin session: defaults are resolved at load, stored values are read in initGui, the user changes a value in the options panel, the change is written immediately, and the next session starts from the stored value. one plugin session load defaults defined in one place initGui() read stored values apply to UI user edits options panel or a dialog apply write immediately survives a crash next start same values nothing to migrate

Step 3 — Separate project-scoped values

Anything that describes the current dataset belongs to the project rather than the user. QGIS provides readEntry and writeEntry for exactly this, and the values travel inside the .qgz file when it is shared.

python
from qgis.core import QgsProject

SECTION = "myplugin"


def project_get(key: str, default: str = "") -> str:
    """Read a project-scoped value; returns `default` when the project has none."""
    value, ok = QgsProject.instance().readEntry(SECTION, key, default)
    return value if ok else default


def project_put(key: str, value: str) -> None:
    """Write a project-scoped value and mark the project dirty so QGIS offers to save."""
    QgsProject.instance().writeEntry(SECTION, key, value)

readEntry returns a (value, ok) tuple; unpacking only the first element is a common slip that turns a missing entry into an empty string with no way to tell the two apart.

Where each kind of setting belongs A grid of four kinds of configuration — a user preference, a dataset-specific choice, a credential, and a machine-specific path — showing the correct store for each and what goes wrong when it is put somewhere else. belongs in if you get it wrong user preference QgsSettings imposed on colleagues dataset choice the project file lost when the project moves credential QgsAuthManager a password in plain text machine path an environment variable breaks on every other machine

Step 4 — Put user settings in the options dialog

Users look for plugin configuration in one of two places: a menu entry under the plugin, or the QGIS options dialog. Registering an options page puts it in the second, which is where most people look first.

python
from qgis.gui import QgsOptionsPageWidget, QgsOptionsWidgetFactory
from qgis.PyQt.QtWidgets import QDoubleSpinBox, QFormLayout


class MyOptionsPage(QgsOptionsPageWidget):
    """The plugin's page inside the QGIS options dialog."""

    def __init__(self, parent=None):
        super().__init__(parent)
        layout = QFormLayout(self)
        self.distance = QDoubleSpinBox()
        self.distance.setRange(0.0, 10000.0)
        self.distance.setValue(get("buffer_distance"))
        layout.addRow("Default buffer distance (m)", self.distance)

    def apply(self) -> None:
        """Called by QGIS when the user accepts the options dialog."""
        put("buffer_distance", self.distance.value())


class MyOptionsFactory(QgsOptionsWidgetFactory):
    def __init__(self):
        super().__init__()
        self.setTitle("My Plugin")

    def createWidget(self, parent=None) -> QgsOptionsPageWidget:
        return MyOptionsPage(parent)

Register the factory in initGui() with iface.registerOptionsWidgetFactory(), keep a reference to it on the plugin, and unregister it in unload(). An options page left registered after unload is a dialog tab that raises on open and cannot be removed without restarting QGIS.

Advanced Patterns

Never store credentials in QgsSettings

A password written with setValue() lands in a plain-text INI file that is readable by anything running as the user, and it is routinely copied when profiles are shared or backed up. QGIS ships QgsAuthManager precisely so plugins do not have to solve this: it keeps credentials in an encrypted database behind a master password and hands out an authentication configuration id that is safe to store.

python
from qgis.core import QgsApplication


def apply_auth(request, auth_cfg_id: str) -> bool:
    """Attach stored credentials to a network request by configuration id."""
    if not auth_cfg_id:
        return True                                # unauthenticated service
    return QgsApplication.authManager().updateNetworkRequest(request, auth_cfg_id)

Store the configuration id in QgsSettings; store the secret nowhere. The network requests and remote data guide covers the request side of this in full.

Migrating a settings schema between releases

The second release of any plugin renames or retypes at least one key. Handle it explicitly with a stored schema version, and run the migration once at load.

python
from qgis.core import QgsSettings

SECTION = "myplugin"
SCHEMA = 2


def migrate() -> None:
    """Bring stored settings up to the current schema. Safe to call on every load."""
    settings = QgsSettings()
    stored = settings.value("%s/schema" % SECTION, 1, type=int)
    if stored >= SCHEMA:
        return

    if stored < 2:
        # v1 stored the distance in kilometres under a different key
        old = settings.value("%s/buffer_km" % SECTION, None, type=float)
        if old is not None:
            settings.setValue("%s/buffer_distance" % SECTION, old * 1000.0)
            settings.remove("%s/buffer_km" % SECTION)

    settings.setValue("%s/schema" % SECTION, SCHEMA)

Removing the old key matters. Leaving it behind means a user who downgrades gets their old setting back with the new one still present, and the next migration has two sources of truth.

What to do when a stored value is not what you expect A decision tree for reading a setting defensively: use the default when the key is absent, coerce and validate when the type is wrong, and fall back to the default and log when the value is out of range. What came back from the settings store? nothing use the default first run wrong type read with type= never trust the string out of range clamp and log a stale schema

Making settings testable

Code that calls QgsSettings() directly is awkward to test because it reads the developer’s own profile. Pass the accessor in, or read through a small object the tests can substitute — the same dependency-injection move that makes iface mockable.

An alternative that needs no injection: set QSettings to use a temporary INI file in a fixture, so every test starts from an empty store and nothing leaks between tests.

Deciding what is configurable at all

Every setting is a promise: to keep supporting that value, to migrate it across releases, and to behave sensibly for every combination a user can produce. A plugin with fifteen settings has a configuration space nobody has tested, and most of it will never be visited by anyone.

The useful discipline is to make a value configurable only once someone has asked for it twice. Until then, pick a sensible default and put it in the code where it is easy to change. This is not laziness — it is the recognition that a hard-coded constant can be revised in one place after a decision, whereas a setting has to keep working for everyone who ever set it.

When a value does become configurable, prefer a small enumerated choice over free text where possible. A dropdown with three options cannot be misspelled, cannot be out of range, and does not need validation code. Free-text paths and URLs are where configuration bugs actually come from, and those deserve the defensive reading described above.

Pitfalls and Debugging

  • Unprefixed keys. A key without a plugin section can collide with a QGIS setting, and removing your plugin leaves it behind forever. Prefix everything.

  • Booleans read as strings. Without type=bool, "false" comes back as a non-empty string, which is truthy. This bug is invisible on Linux and reliable on Windows.

  • Settings written in unload(). They are lost on a crash, which is the session where they matter most.

  • Storing a machine-specific path in the project. An absolute output folder written into the .qgz breaks the moment a colleague opens it. Store a path relative to the project, or keep it in user settings.

  • Options page never unregistered. iface.unregisterOptionsWidgetFactory() must be called in unload(), with the same factory object, or the tab persists into a plugin-less QGIS.

  • Reading settings in __init__. The plugin is constructed before the interface is fully up. Read in initGui() instead, where the profile is guaranteed to be resolved.

Frequently Asked Questions

Should a value go in QgsSettings or the project file?

Ask who owns it. A preference that should follow the person across every project — preferred units, a default folder, a service endpoint they use — belongs in QgsSettings. A choice that describes this dataset — which field to classify on, a threshold tuned to this survey — belongs in the project, so it travels with the file.

Getting this wrong is not a crash, it is a slow annoyance: user preferences in the project impose one person’s setup on everyone who opens it, and dataset choices in user settings quietly apply the wrong threshold to the next project.

Are settings shared between QGIS profiles?

No. Each profile has its own settings store, which is exactly why a second profile is the right way to test first-run behaviour. It also means a user with a work profile and a personal one has two independent copies of your configuration, and neither migration nor defaults carry across.

How do I reset a plugin to its defaults?

Call QgsSettings().remove(SECTION) to drop the whole section in one call, then re-read. Because every read falls back to the declared default, removing the stored values is all a reset needs to do — there is no need to write defaults back.

Offering this in the options page is worth the few lines: it is the fastest way for a user to get out of a broken configuration without hunting for an INI file.

Can I read another plugin’s settings?

Technically yes — the store is global and nothing prevents it. In practice, treat another plugin’s keys as private: they are undocumented, they change without notice, and a plugin that depends on them breaks on somebody else’s release schedule.

Where genuine cooperation is needed, expose a small Python API from your plugin and let the other plugin call it. That is a contract; a settings key is not.

Should defaults differ between platforms?

Only for paths, and even then prefer a computed default over a hard-coded one. A default output folder written as a Windows path is wrong on every other platform, and a default written as a POSIX path is wrong on Windows. Compute it instead — the user’s documents directory, or a subdirectory of the QGIS profile — so one expression produces the right answer everywhere.

Everything else should be identical across platforms. A tolerance or a service URL that differs by operating system is a support problem waiting to happen, because the first thing a bug report omits is which platform it came from.

Where is the settings file on disk?

Under the active profile directory, which QgsApplication.qgisSettingsDirPath() returns. Knowing the path is useful for support — asking a user to attach that file often resolves a configuration report in one round trip — but reading or writing it directly from a plugin is a mistake, because QGIS caches settings in memory and your edit will be overwritten.

Conclusion

Configuration is a small subject with a large failure surface, and nearly all of it is decided by one choice: whether a value belongs to the user, the project, or the machine. Prefix every key, declare defaults once, read with an explicit type, write as values change rather than at shutdown, and keep secrets in the authentication database rather than the settings file. Add a stored schema version early — the release where you need it is not the release where you want to be inventing one.