Reading and Writing Project Variables from Python
Read and write QGIS project variables from Python: the expression scopes they live in, why reading is a map and writing is a call, filtering the read-only…
TL;DR: use QgsExpressionContextUtils.setProjectVariable() to write and the project’s variable map to read — they are user-visible, they travel inside the .qgz file, and every QGIS expression can reference them with an @ prefix, which makes them the right home for values that describe the data rather than the person. This page is part of the working with QgsProject and the layer registry guide.
Complete Runnable Code
"""Read, write and remove project-scoped expression variables."""
from qgis.core import QgsExpressionContextUtils, QgsProject, QgsVectorLayer
def set_project_variable(name: str, value) -> None:
"""Write one project variable and mark the project dirty so QGIS offers to save."""
project = QgsProject.instance()
QgsExpressionContextUtils.setProjectVariable(project, name, value)
project.setDirty(True)
def get_project_variable(name: str, default=None):
"""Read one project variable, returning `default` when it is not set."""
variables = QgsExpressionContextUtils.projectScope(
QgsProject.instance()).variablesToMap()
return variables.get(name, default)
def remove_project_variable(name: str) -> bool:
"""Remove a variable. Returns False when it was not set."""
project = QgsProject.instance()
variables = QgsExpressionContextUtils.projectScope(project).variablesToMap()
custom = {k: v for k, v in variables.items() if not k.startswith("project_")}
if name not in custom:
return False
del custom[name]
QgsExpressionContextUtils.setProjectVariables(project, custom)
project.setDirty(True)
return True
def set_layer_variable(layer: QgsVectorLayer, name: str, value) -> None:
"""Write a variable scoped to one layer rather than the whole project."""
QgsExpressionContextUtils.setLayerVariable(layer, name, value)
QgsProject.instance().setDirty(True)
Architecture Breakdown
Variables are expression-scope entries
A project variable is not a general-purpose key-value store; it is an entry in the project scope of every expression context QGIS builds. That is what gives it its reach: once set, @survey_year is available in the field calculator, in labelling, in symbology rules, in Processing parameters and in layout text, with no further wiring.
It is also what defines its limits. Values must be things an expression can hold — numbers, strings, dates, lists — and names are resolved by a scope search, so a project variable is shadowed by a layer variable of the same name.
Reading is a map, writing is a call
The asymmetry in the API surprises people. Writing goes through QgsExpressionContextUtils.setProjectVariable(), one variable at a time; reading means building the project scope and asking for its map. There is no getProjectVariable().
The map you get back includes QGIS’s own read-only entries — project_folder, project_title, project_crs and others, all prefixed project_. Filtering those out, as remove_project_variable does, is necessary whenever you write the whole set back, because writing them back as custom variables makes them shadow the real ones.
Three scopes, resolved in order
Variables exist at three levels, and knowing which level a name came from explains most of the surprising behaviour in this area.
Global variables live in the user profile and follow the person. Project variables live in the project file and follow the data. Layer variables live with the layer inside the project. An expression resolves the most specific first, so a layer variable wins over a project one of the same name, which is a useful override mechanism and an easy source of confusion when it is accidental.
Choosing Between Variables and Entry Storage
Two project-scoped stores exist and they are not interchangeable, so it is worth deciding deliberately rather than reaching for whichever comes to mind first.
QgsProject.writeEntry() is the other project-scoped store, and the two are not interchangeable. Entry storage takes a plugin-namespaced key and is invisible to users and to expressions — right for structured plugin state that nobody should edit by hand. Variables are visible in the project properties dialog and editable there, which makes them right for values a user is expected to set.
The practical test is whether you would be happy for a user to change the value in the interface. A survey year, a client name, an output prefix: yes, and they belong in a variable. A serialised cache of layer identifiers: no, and it belongs in entry storage.
Using a Variable Downstream
from qgis.core import QgsExpression, QgsExpressionContext, QgsExpressionContextUtils
def evaluate_with_project_scope(expression_text: str):
"""Evaluate an expression that references project variables."""
context = QgsExpressionContext()
context.appendScope(QgsExpressionContextUtils.globalScope())
context.appendScope(QgsExpressionContextUtils.projectScope(QgsProject.instance()))
expression = QgsExpression(expression_text)
if expression.hasParserError():
raise ValueError(expression.parserErrorString())
return expression.evaluate(context)
set_project_variable("survey_year", 2026)
print(evaluate_with_project_scope("'Survey ' || @survey_year")) # Survey 2026
Note that the context needs the project scope appended explicitly. An expression referencing @survey_year against an empty context resolves it to NULL and reports no error at all, which is the same trap described in the expression engine guide.
Making Variables Discoverable
A variable nobody knows about is a variable nobody uses, and the project properties dialog is a long list by the time a project has been through a few hands. Two small habits make a plugin’s variables findable.
Prefix them consistently — myplugin_survey_year rather than survey_year — so every value your plugin depends on sorts together and is obviously related. The cost is a longer name in expressions; the benefit is that somebody opening the project in a year can tell what belongs to what.
Write sensible defaults on first use rather than waiting for the user to create them. A plugin that sets its variables to defaults when it first runs against a project gives the user something to edit, which is far more discoverable than documentation explaining which variables they could create.
Production Best Practices
- Mark the project dirty after writing, or the change is lost when the user closes without saving.
- Filter out
project_-prefixed entries before writing a variable map back. - Namespace your variable names if the plugin sets several, so they are recognisable in the project properties dialog.
- Do not store secrets in variables. They are plain text inside the project file and travel with it.
- Prefer entry storage for plugin state that users should not edit.
- Read defensively. A project that has never had your variable set is the normal case, not an error.
Frequently Asked Questions
Do project variables survive saving and reopening?
Yes — they are serialised into the .qgz file and restored with it, which is the whole point. What they do not do is follow the data: opening the same layers in a different project gives you a different set of variables.
Can I set a variable before a project is loaded?
You can set it on the current empty project, and it will be discarded when another project is opened, because opening a project replaces the whole variable set. For a plugin that wants to apply variables to whatever project is loaded, connect to QgsProject.instance().readProject and set them there.
Why does my variable not appear in the field calculator?
Most often because the expression context in the calculator was built before the variable was set — reopening the dialog rebuilds it. If it still does not appear, check for a layer variable of the same name shadowing it, and check that the name has no spaces, which the expression parser cannot address.
Are variable names case-sensitive?
Yes. @surveyYear and @surveyyear are different variables, and only one of them is the one you set. Sticking to lowercase with underscores avoids the whole question and matches the convention QGIS uses for its own variables.
Can a variable hold a list or a map?
Yes — expression arrays and maps are valid values, and they round-trip through the project file. They are awkward to edit in the properties dialog, so a structured value is often a sign that the data belongs in entry storage instead, where you can serialise it as JSON without pretending it is user-editable.
How do I list every custom variable a project has?
Take the project scope’s variable map and drop the project_-prefixed keys, exactly as remove_project_variable does. That difference is the set your plugin or your users have added, and it is worth logging when diagnosing a project that behaves differently from another.
Related
- Working with QgsProject and the Layer Registry — the parent guide covering the project singleton
- Attribute Data and the Expression Engine — the scope search that resolves an at-prefixed name
- Plugin Settings and Configuration Management — the other place a configuration value can live