Add a Field to a Layer in PyQGIS

Adding a column is the first step of almost every attribute workflow, and it fails in the same two ways for almost everyone: the field is created but the layer insists it does not exist, or the type turns out to be wrong once real values arrive. Both are avoidable with two extra lines — one to refresh the layer's schema cache, and one to check the provider can do what you are asking before you ask.

This page is a focused recipe within Attribute Tables and Field Management in PyQGIS. It covers adding one or several fields, choosing types, making the script idempotent, and the format-specific limits that bite when the target is a shapefile.

Why updateFields() is not optionalA script checks that the provider supports adding attributes, then calls addAttributes, which changes the data source. The layer object still holds a cached field list at that point, so a lookup by name fails. Calling updateFields re-reads the schema into the layer, after which the field resolves correctly.The data source changes first; the layer object finds out secondcapabilities()AddAttributes?addAttributes()writes to the sourcelayer.fields()still the old listupdateFields()re-reads the schemaindexOf()now resolvesskip updateFields() and indexOf() returns -1 for a field that genuinely exists

Prerequisites

  • QGIS 3.34 LTR (bundled Python 3.12) or newer.
  • A writable vector layer. GeoPackage is the recommended target; a WFS or delimited-text layer will reject the operation.
  • Familiarity with the Python Console — see QGIS Python Console Basics.

Add a single field

Three lines, and the third is the one people forget.

from qgis.core import QgsField, QgsProject
from qgis.PyQt.QtCore import QVariant

layer = QgsProject.instance().mapLayersByName("parcels")[0]

layer.dataProvider().addAttributes([QgsField("value_per_m2", QVariant.Double)])
layer.updateFields()

print(layer.fields().names())

Breakdown: addAttributes() takes a list, even for one field, and writes straight to the data source with no undo. QVariant.Double is a Qt type rather than a Python one — the mapping is given in the table below. updateFields() re-reads the schema into the layer object; without it, layer.fields() keeps returning the pre-change list and every lookup by name fails with a confusing -1.

Choose the right type

The type is fixed once the field exists, and changing it later means creating a new field and copying values across. It is worth a moment's thought.

DataQt typeNotes
Whole numbersQVariant.Int32-bit; use QVariant.LongLong for identifiers above two billion
DecimalsQVariant.DoubleSet prec for the number of decimal places the provider stores
TextQVariant.StringSet len on formats that require it; a shapefile caps names at 10 characters
True/falseQVariant.BoolSupported by GeoPackage and PostGIS; a shapefile stores it as an integer
A dateQVariant.DateNot available in shapefile with a time component — use QVariant.DateTime on GeoPackage
from qgis.core import QgsField
from qgis.PyQt.QtCore import QVariant

fields = [
    QgsField("parcel_ref", QVariant.String, len=32),
    QgsField("value_per_m2", QVariant.Double, len=12, prec=2),
    QgsField("surveyed_on", QVariant.Date),
    QgsField("is_exempt", QVariant.Bool),
]
layer.dataProvider().addAttributes(fields)
layer.updateFields()

Breakdown: Adding several fields in one addAttributes() call is a single provider round-trip rather than four, which matters on a network database. len and prec are honoured by providers that store them and ignored by those that do not, so setting them costs nothing and helps where it counts. Note prec=2 on a Double means the stored precision — it does not round values you write, it declares how many decimals the column keeps.

Make it safe to re-run

A script that fails the second time it runs is a script nobody trusts. Guard both the capability and the existence.

from qgis.core import QgsField, QgsProject, QgsVectorDataProvider
from qgis.PyQt.QtCore import QVariant

WANTED = {
    "value_per_m2": (QVariant.Double, 12, 2),
    "rate_band": (QVariant.String, 4, 0),
}

layer = QgsProject.instance().mapLayersByName("parcels")[0]
provider = layer.dataProvider()

if not provider.capabilities() & QgsVectorDataProvider.AddAttributes:
    raise RuntimeError(f"{layer.name()} does not support adding fields")

existing = set(layer.fields().names())
to_add = [
    QgsField(name, qtype, len=length, prec=precision)
    for name, (qtype, length, precision) in WANTED.items()
    if name not in existing
]

if to_add:
    provider.addAttributes(to_add)
    layer.updateFields()
    print("added:", [f.name() for f in to_add])
else:
    print("nothing to add")

Breakdown: The capability check turns a silent no-op into an explicit failure with the layer's name in it — far easier to diagnose than discovering later that nothing was written. Building to_add from a set difference means the script is idempotent: run it ten times and the schema is identical. Declaring the wanted schema as a dictionary at the top also documents it, which matters when the script is the only record of what the table should look like.

Populate the new field

A field with no values is NULL everywhere. Filling it in the same pass keeps the schema and data in step.

Two routes from an empty column to a filled oneAn added but empty field can be filled two ways. The Python route iterates the features, builds a dictionary of feature identifier to value, and applies it in a single changeAttributeValues call. The Processing route skips the separate add step entirely, using native:fieldcalculator to create and populate the column in one operation, writing a new layer.Add then fill, or do both at oncefield addedevery value NULLPython routeloop → {fid: {idx: value}}one changeAttributeValues callProcessing routenative:fieldcalculatoradds and fills in one passedits the layer in placefull Python controlwrites a new layersafe to re-run

index = layer.fields().indexOf("value_per_m2")
updates = {}
for feature in layer.getFeatures():
    area = feature.geometry().area()
    value = feature["assessed_value"]
    if area and value is not None:
        updates[feature.id()] = {index: round(value / area, 2)}

layer.dataProvider().changeAttributeValues(updates)

Breakdown: Resolving the index after updateFields() is essential — before it, indexOf() returns -1 and changeAttributeValues() silently writes nothing. The dictionary maps feature ID to a dictionary of field index to value, which is the shape the provider expects, and building it first turns n round-trips into one. The guard skips features with no geometry or a NULL value rather than writing garbage. See Update Attribute Values in Bulk with PyQGIS for the transactional alternative.

Format limits worth knowing

The provider decides what is possible, and the differences are large enough to change which format you choose. A script that works perfectly against GeoPackage can quietly mangle the same schema written to a shapefile.

What each format does to your schemaA three-column comparison. GeoPackage and PostGIS both allow long field names, a true boolean type, full date and time support and reliable NULL storage. Shapefile truncates names to ten characters, stores booleans as integers, has no time component on dates and handles NULL inconsistently.The format quietly rewrites your schema — pick it deliberatelycapabilityGeoPackagePostGISShapefilefield name lengthunrestricted63 chars10 chars — truncatedboolean typenativenativestored as integerdate with timeyesyesdate onlyNULL vs empty stringdistinctdistinctindistinguishablea schema that round-trips through shapefile does not come back the same

If a shapefile is unavoidable, verify the names actually survived rather than assuming they did:

WANTED = ["value_per_square_metre", "surveyed_on"]

layer.dataProvider().addAttributes([...])
layer.updateFields()

actual = layer.fields().names()
for name in WANTED:
    if name not in actual:
        close = [a for a in actual if a.startswith(name[:8])]
        print(f"'{name}' became {close!r}")

Breakdown: Comparing the requested names against layer.fields().names() after the fact is the only reliable check, because addAttributes() reports success even when the driver renames what it stored. Matching on a truncated prefix identifies what the name became, which is what any downstream code will have to reference. The cleaner answer, wherever you have the choice, is to write GeoPackage — the format costs nothing extra and removes all four limitations at once.

QGIS version compatibility

The examples target QGIS 3.34 LTR (Python 3.12).

QGIS versionPythonNotes
3.28 LTR3.9Identical API. QVariant types unchanged.
3.34 LTR3.12Baseline for this page.
3.40 / 3.443.12QgsField accepts QMetaType alongside QVariant; the QVariant form still works.

addAttributes(), updateFields() and the capability flags have been stable across 3.x.

Troubleshooting

  • indexOf() returns -1 for a field that exists. updateFields() was not called after addAttributes().
  • addAttributes() returns False. The provider does not support it. Check capabilities() & QgsVectorDataProvider.AddAttributes and confirm the file is not read-only on disk.
  • The field name was truncated. The target is a shapefile, which caps field names at ten characters. Write to GeoPackage instead.
  • Values come back as text when the field is numeric. The field was created as QVariant.String. Types cannot be changed in place — add a correctly typed field, copy the values across, and drop the old one.
  • A date field rejects values. Assign a Python datetime.date, or a QDate, not a string. A string only works if the provider silently coerces it, which not all do.
  • The script fails on its second run. No existence check. Filter the wanted fields against layer.fields().names() before adding.

Conclusion

Adding a field is addAttributes() followed by updateFields(), with a capability check in front and an existence check to keep the script re-runnable. Spend the extra moment on the type — it is the one decision you cannot revise without rebuilding the column — and prefer GeoPackage over shapefile so names and types survive intact.

Frequently Asked Questions

Why does my new field not show up? The provider has it but the layer object is serving a cached schema. Call layer.updateFields() straight after addAttributes() and lookups by name will resolve.

Which QVariant type should I use for decimals?QVariant.Double, with len and prec set if the format stores them. prec declares how many decimal places the column keeps; it does not round the values you write.

Can I change a field's type after creating it? Not in place. Add a new field with the correct type, copy the values across with changeAttributeValues(), then delete the original — remembering to delete by descending index if you remove several.

Can I add several fields in one call? Yes, and you should — addAttributes() takes a list, so passing four QgsField objects together is one provider round-trip instead of four. Call updateFields() once afterwards.

Do I need to start an edit session to add a field? No, if you go through layer.dataProvider().addAttributes(), which writes immediately. Use layer.startEditing() and layer.addAttribute() only when the change should be undoable, such as inside a plugin.

Is there a limit on how many fields a layer can have? Format-dependent. A shapefile caps out at 255 attribute fields, GeoPackage allows far more, and PostGIS effectively has no practical limit. A table approaching any of those limits is usually better normalised into related tables than widened further.