How to Add a Field and Populate It with an Expression in PyQGIS
Add a column to a QGIS vector layer from Python and fill it from an expression: the provider schema change, updateFields, a prepared QgsExpression, and a…
TL;DR: add the column through the data provider, call updateFields(), then evaluate a prepared QgsExpression per feature inside a single edit session and commit once — the provider handles the schema, the edit buffer handles the values, and mixing the two up is what makes this fail. This page is part of the attribute data and the expression engine guide.
Complete Runnable Code
"""Add a numeric field to a layer and fill it from a QGIS expression."""
from qgis.core import (QgsExpression, QgsExpressionContext, QgsExpressionContextUtils,
QgsField, QgsVectorLayer)
from qgis.PyQt.QtCore import QVariant
def add_calculated_field(layer: QgsVectorLayer, name: str, expression_text: str,
field_type: QVariant.Type = QVariant.Double) -> int:
"""Add `name` to `layer` and populate it by evaluating `expression_text`.
Returns the number of features written. Raises on a parse error, on a
refused schema change, or on a failed commit — never leaves the layer in an
open edit session.
"""
expression = QgsExpression(expression_text)
if expression.hasParserError():
raise ValueError("cannot parse %r: %s" % (expression_text,
expression.parserErrorString()))
# 1. schema change: goes through the provider, is immediate, is not undoable
if layer.fields().indexFromName(name) < 0:
if not layer.dataProvider().addAttributes([QgsField(name, field_type)]):
raise RuntimeError("provider refused to add field %r" % name)
layer.updateFields() # refresh the layer's cached schema
index = layer.fields().indexFromName(name)
if index < 0:
raise RuntimeError("field %r still missing after updateFields()" % name)
# 2. one context, prepared once — not once per feature
context = QgsExpressionContext()
context.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(layer))
expression.prepare(context)
written = 0
layer.startEditing()
try:
for feature in layer.getFeatures():
context.setFeature(feature)
value = expression.evaluate(context)
if expression.hasEvalError():
raise RuntimeError("feature %s: %s" % (feature.id(),
expression.evalErrorString()))
if layer.changeAttributeValue(feature.id(), index, value):
written += 1
if not layer.commitChanges():
raise RuntimeError("commit failed: %s" % "; ".join(layer.commitErrors()))
except Exception:
layer.rollBack()
raise
return written
Call it with any valid QGIS expression — "$area / 10000", "population / area_km2", "upper(\"name\")" — and the field type that matches what the expression returns.
Architecture Breakdown
dataProvider().addAttributes() — the schema half
A new column is a change to the file on disk, not a change to a row, so it does not go through the edit buffer at all. addAttributes() writes it immediately and returns a boolean; there is no undo, and no commit is required or possible.
The call that is easy to forget is updateFields(). Without it the provider has the new column but the layer’s cached fields() does not, so indexFromName() returns -1 for a field that visibly exists in the file — and -1 is accepted by changeAttributeValue(), where it silently does nothing.
QgsExpression.prepare() — resolving names once
Preparing an expression walks its parsed tree and binds every field reference to a column index using the fields visible in the context. After that, each evaluation is an array lookup rather than a name lookup, which on a large layer is the difference between a job that takes seconds and one that takes minutes.
The context you prepare with must carry the same fields as the contexts you later evaluate with. Preparing against one layer and evaluating against another with a different schema does not raise — it reads the wrong columns.
changeAttributeValue() — the buffered half
This writes into the edit session’s change buffer and pushes an entry onto the undo stack. Nothing reaches disk until commitChanges(), which is what makes the whole operation atomic: an exception halfway through leaves the file untouched, provided you roll back.
commitChanges() reports failure through its return value rather than an exception. Ignoring it produces a script that logs success and changes nothing, usually because a constraint or a read-only provider rejected the write.
Handling a Failure on One Feature
Not every evaluation error deserves the same response, and deciding which you have is the difference between a run that stops usefully and one that either dies on the first gap in the data or writes nonsense for a thousand rows.
The example above treats any evaluation error as fatal, which is the right default: an expression that fails on one row usually fails on the next thousand. When the input genuinely contains gaps — a ratio whose denominator is sometimes NULL — the correct behaviour is to write NULL for that feature and carry on, which QGIS expression semantics already do without raising. Reserve the hard failure for errors that indicate the expression itself is wrong.
Why the Order of Those Calls Matters
The sequence in the example is not a style choice. A schema change and a value change travel through two different halves of the QGIS layer API, and each has its own rules about when it takes effect.
The provider write happens immediately and cannot be undone, which is why it comes first: if the column cannot be created — a read-only file, an unsupported type, a name the format rejects — you want to know before any values have been computed. Doing it inside the edit session instead produces the worst outcome available, where the column is created, the commit then fails, and the layer is left with a new empty field nobody asked for.
The value writes happen in the buffer and take effect together. That is what makes the operation recoverable: an exception on feature nine hundred rolls back the previous eight hundred and ninety-nine, and the file on disk is exactly as it was. Skipping the rollback breaks that guarantee in a particularly unhelpful way, because the layer stays in an open edit session and the next piece of code to call startEditing() inherits the partial changes as though they were its own.
Running It Headlessly
Nothing here requires a GUI. Inside a standalone script, bootstrap the application first and open the layer directly:
from qgis.core import QgsApplication, QgsVectorLayer
QgsApplication.setPrefixPath("/usr", True)
app = QgsApplication([], False)
app.initQgis()
try:
layer = QgsVectorLayer("/data/parcels.gpkg|layername=parcels", "parcels", "ogr")
if not layer.isValid():
raise SystemExit("could not open the layer")
print(add_calculated_field(layer, "area_ha", "$area / 10000"))
finally:
app.exitQgis()
The standalone scripts and headless execution guide covers the bootstrap in full, including the environment variables a container needs.
Production Best Practices
- Check
indexFromName()twice — before adding, to avoid a duplicate, and afterupdateFields(), to confirm the provider actually accepted the change. - Prepare the expression once, outside the loop. It is one line and worth an order of magnitude.
- Always roll back on the error path. A layer left in an open edit session poisons whatever runs next.
- Prefer a real column over
$areain expressions that run at scale. Geometric functions are recomputed per evaluation and cannot be pushed down to the provider. - Mind the shapefile field-name limit. Ten characters, silently truncated, and every expression referencing the full name then resolves to NULL.
- Write the expression text into the layer or project metadata when the value must be reproducible later. A column with no record of how it was derived is a number nobody can defend.
Frequently Asked Questions
Why is my new field full of NULLs?
Either the expression referenced a field name that does not resolve — check layer.fields().names() against exactly what you typed — or the context had no feature scope when it was evaluated. Both produce NULL rather than an error, which is why the eval-error check in the example matters.
Can I do this with a Processing algorithm instead?
Yes: native:fieldcalculator takes the same expression and writes a new layer. That is the better choice inside a pipeline, because it leaves the input untouched. Use the in-place approach above when you specifically want to modify an existing dataset and keep its identifiers.
Does this work on a shapefile?
It does, with two caveats: field names are truncated to ten characters, and adding a field rewrites the whole DBF, which is slow on large files. GeoPackage has neither limitation and is the better target for anything you control.
How do I update an existing field instead of adding one?
Skip the provider call entirely and resolve the index of the field that is already there. The rest of the function is unchanged — the edit buffer does not care whether the column is new. What does change is the risk profile: overwriting a populated column destroys whatever was in it, and the undo stack only protects you until the commit.
For anything irreversible, run it on a copy first and compare. native:fieldcalculator writing to a new layer is the safer shape when the values matter.
Can I write to several fields in one pass?
Yes, and you should when more than one derived value comes from the same iteration. Prepare one expression per target field, evaluate them all inside the same feature loop, and use changeAttributeValues() — plural — to apply a whole attribute map for a feature in a single call. One pass over the data instead of three is the entire saving, and on a large layer it is a large one.
Related
- Attribute Data and the Expression Engine — the parent guide covering contexts, scopes and write paths
- Filtering Features with QgsFeatureRequest Expressions — the same expression syntax, evaluated by the provider
- Optimizing Feature Iteration with QgsVectorLayer.getFeatures — constraining the iteration this loop depends on