Set Default Values and Field Constraints in PyQGIS
Editor widgets control how a value is entered. Defaults and constraints control what ends up stored. A default fills a field before the user sees the form — the date, the name of whoever is logged in, a unique identifier, the area of the polygon just drawn. A constraint refuses to let a feature be saved unless a rule holds: the field is not empty, the value is unique, an expression evaluates true. Together they turn a layer from a place where data is typed into a place where data is checked.
This recipe belongs to Attribute Forms & Layer Actions in PyQGIS. It sets expression defaults, including ones that update on every edit, adds the three constraint types with hard and soft strength, and shows how to apply the same rules to features that a script creates.
Prerequisites
- QGIS 3.40 LTR or newer, or the QGIS 4 series.
- An editable vector layer. Constraints are stored per layer in the project or its style; database-level constraints are read from the provider automatically.
- Familiarity with QGIS expressions, which drive both defaults and expression constraints.
Expression defaults
A default is a QgsDefaultValue: an expression, plus a flag saying whether it is recalculated whenever the feature changes. It is set per field index.
from qgis.core import QgsProject, QgsDefaultValue
inspections = QgsProject.instance().mapLayersByName("inspections")[0]
def set_default(layer, field_name, expression, apply_on_update=False):
idx = layer.fields().indexOf(field_name)
if idx < 0:
raise KeyError(field_name)
layer.setDefaultValueDefinition(idx, QgsDefaultValue(expression, apply_on_update))
set_default(inspections, "inspection_uid", "uuid('WithoutBraces')")
set_default(inspections, "created_at", "now()")
set_default(inspections, "created_by", "@user_account_name")
set_default(inspections, "modified_at", "now()", apply_on_update=True)
set_default(inspections, "modified_by", "@user_full_name", apply_on_update=True)
set_default(inspections, "area_m2", "round($area, 1)", apply_on_update=True)
Breakdown: Without apply_on_update, a default is evaluated once, when the feature is created, and never again — right for creation timestamps and identifiers. With it, the expression runs every time the feature is edited, which is how audit fields and derived values like area stay current. $area respects the project's ellipsoid and distance units, so set those deliberately before relying on a stored area. @user_account_name is the operating system login and @user_full_name the display name; both are evaluated on the editing machine, which is exactly what an audit trail needs. uuid('WithoutBraces') produces identifiers that stay unique across offline editors merging data later, which sequential numbers cannot promise.
A tempting default for a sequential id is maximum("inspection_no") + 1. It works for one editor and breaks for two: both compute the same maximum before either commits. If the numbers must be sequential, let the database assign them with a sequence and read them back after the commit; if they only need to be unique, use a UUID.
Database defaults
Layers backed by PostGIS or GeoPackage may already have defaults defined in the table — a serial key, DEFAULT now(). QGIS reads those as provider defaults and shows the SQL clause in the form, such as nextval('inspections_id_seq'), until the feature is committed.
from qgis.core import Qgis
provider = inspections.dataProvider()
for idx, field in enumerate(inspections.fields()):
clause = provider.defaultValueClause(idx)
if clause:
print(f"{field.name():<16} provider default: {clause}")
QgsProject.instance().setFlag(Qgis.ProjectFlag.EvaluateDefaultValuesOnProviderSide, True)
Breakdown: defaultValueClause returns the database's own default expression, which the provider will apply on insert. Setting the project flag asks QGIS to evaluate those defaults immediately when the feature is created, so users see the actual next id rather than the clause text — at the cost of a round trip to the database per new feature, and of consuming a sequence value even if the user cancels. A layer-level QgsDefaultValue on the same field overrides the database default in the form, so avoid setting both on key fields.
Constraints: not null, unique and expressions
Constraints are rules checked in the form and when edits are committed. Each has a strength: hard constraints block the feature from being accepted, soft constraints show a warning but let the user continue.
from qgis.core import QgsFieldConstraints
HARD = QgsFieldConstraints.ConstraintStrength.ConstraintStrengthHard
SOFT = QgsFieldConstraints.ConstraintStrength.ConstraintStrengthSoft
def idx(name):
return inspections.fields().indexOf(name)
inspections.setFieldConstraint(
idx("asset_id"), QgsFieldConstraints.Constraint.ConstraintNotNull, HARD)
inspections.setFieldConstraint(
idx("inspection_uid"), QgsFieldConstraints.Constraint.ConstraintUnique, HARD)
inspections.setConstraintExpression(
idx("condition"),
'"condition" BETWEEN 1 AND 5',
"Condition must be scored 1 (worst) to 5 (best)",
)
inspections.setFieldConstraint(
idx("condition"), QgsFieldConstraints.Constraint.ConstraintExpression, HARD)
inspections.setConstraintExpression(
idx("notes"),
'"condition" > 2 OR length(coalesce("notes", \'\')) >= 20',
"Poor or worse condition needs a note of at least 20 characters",
)
inspections.setFieldConstraint(
idx("notes"), QgsFieldConstraints.Constraint.ConstraintExpression, SOFT)
Breakdown: setConstraintExpression stores the rule and the message users see; setFieldConstraint with ConstraintExpression sets its strength. Expression constraints can reference other fields, which is what makes the conditional rule on notes possible — the note is only demanded when the condition is poor. Making that one soft is a judgement call: it nudges field crews without stopping them saving an urgent record when they are standing in the rain. Unique constraints are checked against the whole layer, including uncommitted edits, which can be slow on very large tables; a unique index in the database is the stronger and faster guarantee where one is available.
To see what a field is subject to, including rules that came from the database, read its constraints object:
field = inspections.fields().field("asset_id")
constraints = field.constraints()
for c in (QgsFieldConstraints.Constraint.ConstraintNotNull,
QgsFieldConstraints.Constraint.ConstraintUnique,
QgsFieldConstraints.Constraint.ConstraintExpression):
if constraints.constraints() & c:
origin = constraints.constraintOrigin(c)
print(c, "origin:", origin, "strength:", constraints.constraintStrength(c))
Breakdown: constraintOrigin distinguishes ConstraintOriginProvider — the database said so — from ConstraintOriginLayer — somebody set it in QGIS. Provider constraints cannot be removed from QGIS, only from the database schema. When a project must also be safe against edits from other tools, move the important rules into the database and let QGIS pick them up.
Validate features created by scripts
Defaults and constraints are applied by the form. A script that builds a QgsFeature and calls addFeature sees neither — unless it asks. QgsVectorLayerUtils creates features with defaults evaluated and validates attributes against every constraint.
from qgis.core import QgsGeometry, QgsPointXY, QgsVectorLayerUtils, edit
geom = QgsGeometry.fromPointXY(QgsPointXY(429812.4, 433120.7))
attrs = {idx("asset_id"): "TR-10432", idx("condition"): 2}
feature = QgsVectorLayerUtils.createFeature(
inspections, geom, attrs, inspections.createExpressionContext())
problems = []
for i in range(inspections.fields().count()):
ok, errors = QgsVectorLayerUtils.validateAttribute(inspections, feature, i)
if not ok:
problems.append((inspections.fields()[i].name(), errors))
if problems:
print("rejected:", problems)
else:
with edit(inspections):
inspections.addFeature(feature)
Breakdown: createFeature fills every field that has a default — UUID, timestamps, user — and then applies the attributes you pass, so explicit values win. validateAttribute checks all constraints on one field, hard and soft, and returns the messages defined with the expressions, which makes the rejection log readable. In this example the soft rule on notes fails because condition 2 has no note; deciding whether a script should honour soft rules as strictly as hard ones is a policy choice, and passing a strength argument to validateAttribute lets you check hard constraints only. The edit context manager commits on success and rolls back on an exception, as covered in editing features with transactions.
QGIS version compatibility
QgsDefaultValue with applyOnUpdate has existed since 3.2. The scoped enums QgsFieldConstraints.Constraint.ConstraintNotNull and QgsFieldConstraints.ConstraintStrength.ConstraintStrengthHard work on all 3.x releases and are required on the QGIS 4 series. Qgis.ProjectFlag.EvaluateDefaultValuesOnProviderSide replaced setEvaluateDefaultValues in 3.26. uuid('WithoutBraces') accepts its format argument from 3.18; older releases always return braces.
Troubleshooting
- A default never appears. The field is filled by a provider default that overrides it, or the feature was created by a script without
createFeature. modified_atdoes not change.applyOnUpdatewas left False.- Users can save despite a failing rule. The constraint is soft, or only the expression was set without
setFieldConstraintfor its strength. - The form shows
nextval(…)as text. That is the provider default clause; enable provider-side evaluation to see the value. - Unique checks make saving slow. The layer is large; enforce uniqueness in the database instead.
Conclusion
Use expression defaults for identifiers, audit fields and derived values, with applyOnUpdate for anything that must follow later edits. Add not-null, unique and expression constraints with hard strength for rules that must hold and soft strength for guidance. Push the non-negotiable rules into the database, and route script-created features through QgsVectorLayerUtils so they meet the same standard as features entered by hand.
Frequently Asked Questions
Can a default reference other fields of the same feature?
Yes. Field references in a default expression read the current values, which is why applyOnUpdate defaults can derive one field from others.
Do constraints check existing features?
Only when those features are edited. To audit a whole layer, loop over it with validateAttribute.
Are defaults and constraints saved in QML styles?
Yes, under the Fields and Forms style categories, so they travel with a style saved to the data source.
Can I show the constraint message in another language? The message is stored as text; set it from your plugin using its translation function when you configure the layer.