Delete and Rename Fields in PyQGIS
Deleting a field looks symmetrical with adding one, and it is not. Field indexes are positional: remove the field at index 3 and every field after it shifts down by one. A loop that resolves an index, deletes it, resolves the next and deletes that will remove the wrong columns — and because the operation succeeds, nothing tells you until the data is already gone. Renaming has its own asymmetry: not every provider supports it, and the ones that do not fail silently.
This page is a focused recipe within Attribute Tables and Field Management in PyQGIS. It covers deleting one or several fields safely, renaming, keeping a whitelist rather than a blacklist, and the non-destructive alternative that writes a clean copy.
Prerequisites
- QGIS 3.34 LTR (bundled Python 3.12) or newer.
- A writable vector layer, and a backup. Field deletion through the provider is immediate and cannot be undone.
- The field names you intend to remove, not their indexes — resolve indexes at the moment of deletion.
Delete fields safely
Resolve every index first, filter out the misses, and delete in descending order.
from qgis.core import QgsProject, QgsVectorDataProvider
layer = QgsProject.instance().mapLayersByName("parcels")[0]
provider = layer.dataProvider()
if not provider.capabilities() & QgsVectorDataProvider.DeleteAttributes:
raise RuntimeError(f"{layer.name()} does not support deleting fields")
targets = ["tmp_a", "tmp_b", "scratch"]
indexes = [layer.fields().indexOf(name) for name in targets]
missing = [name for name, i in zip(targets, indexes) if i == -1]
indexes = sorted((i for i in indexes if i != -1), reverse=True)
if missing:
print("not present, skipping:", ", ".join(missing))
if indexes:
provider.deleteAttributes(indexes)
layer.updateFields()
print("remaining fields:", layer.fields().names())
Breakdown: Resolving all the indexes before any deletion is what makes the descending sort valid — every index refers to the original schema. Sorting descending means each removal only shifts indexes that have already been handled. indexOf() returns -1 for a missing field, and passing that to deleteAttributes() is silently ignored, so filtering it out and reporting it separately turns a hidden no-op into visible information. updateFields() refreshes the layer's cached schema, exactly as it does after adding.
Keep a whitelist instead
Naming what to remove breaks as soon as an upstream dataset gains a column. Naming what to keep is stable, and is usually what a pipeline actually means.
KEEP = {"parcel_id", "owner", "zoning", "assessed_value"}
indexes = sorted(
(i for i, field in enumerate(layer.fields()) if field.name() not in KEEP),
reverse=True,
)
if indexes:
layer.dataProvider().deleteAttributes(indexes)
layer.updateFields()
Breakdown: enumerate(layer.fields()) gives index and field together, so the whitelist test and the index come from one pass. The descending sort is still required. This form is the right default for a script that consumes data you do not control — a new column appearing upstream is dropped automatically rather than silently propagating through your outputs.
Rename a field
Renaming is a separate provider capability, and support is uneven: GeoPackage and PostGIS handle it, shapefile does not.
from qgis.core import QgsProject, QgsVectorDataProvider
layer = QgsProject.instance().mapLayersByName("parcels")[0]
provider = layer.dataProvider()
if not provider.capabilities() & QgsVectorDataProvider.RenameAttributes:
raise RuntimeError("this provider cannot rename fields")
index = layer.fields().indexOf("assessed_value")
if index == -1:
raise KeyError("field not found")
if provider.renameAttributes({index: "value_assessed"}):
layer.updateFields()
print(layer.fields().names())
Breakdown: renameAttributes() takes a dictionary of index to new name, so several renames go in one call. It returns a bool — False when a name collides with an existing field or the provider refuses — and checking it is the only signal. Bear in mind that renaming breaks every expression, style rule and join that referenced the old name; those are stored in the project rather than the data source, so they do not follow the change.
When you only want a friendlier label in the interface, an alias is the non-destructive alternative and nothing downstream breaks:
layer.setFieldAlias(index, "Assessed value (£)")
Breakdown: The alias changes only the display label. Expressions, joins and styles continue to reference the underlying name, which is exactly what makes it safe. Aliases live in the QGIS project rather than the data source, so they travel with the .qgz and not with the file.
The non-destructive alternative
If the source data is not yours to modify — or you want the operation to be re-runnable — write a trimmed copy instead of editing in place.
import processing
processing.run("native:deletecolumn", {
"INPUT": "/data/parcels.gpkg|layername=parcels",
"COLUMN": ["tmp_a", "tmp_b", "scratch"],
"OUTPUT": "/data/output/parcels_trim.gpkg",
})
Breakdown: COLUMN takes a list of field names, so there is no index arithmetic and no ordering hazard — the algorithm resolves them internally. A name that does not exist is ignored rather than raising, which makes the call safe against schema drift. Because it writes a new layer, the operation is re-runnable and the original stays available. This is the form to prefer inside a chained Processing pipeline, where a destructive in-place edit would make the chain impossible to replay.
Check what the field is used by first
Deleting a column that a style rule, a label expression or a join depends on leaves a project that opens with broken symbology and no obvious cause. A short audit before the delete finds those references.
from qgis.core import QgsProject
FIELD = "zoning"
layer = QgsProject.instance().mapLayersByName("parcels")[0]
warnings = []
renderer = layer.renderer()
if FIELD in (renderer.dump() or ""):
warnings.append("referenced by the renderer")
labeling = layer.labeling()
if labeling is not None and FIELD in str(labeling.settings().fieldName):
warnings.append("referenced by a labelling rule")
for join in layer.vectorJoins():
if FIELD in (join.targetFieldName(), join.joinFieldName()):
warnings.append(f"used as a join key with {join.joinLayerId()}")
print(f"{FIELD}: " + ("; ".join(warnings) if warnings else "no references found"))
Breakdown: renderer.dump() returns a text description of the renderer including any expressions it uses, which makes a substring test a crude but effective first pass. layer.vectorJoins() lists the virtual joins configured on the layer, and a field used as a join key is the most damaging one to remove. None of these checks is exhaustive — a field can also appear in a layout item or another layer's expression — but together they catch the great majority, and a "no references found" result on a field you were unsure about is genuinely reassuring.
QGIS version compatibility
The examples target QGIS 3.34 LTR (Python 3.12).
| QGIS version | Python | Notes |
|---|---|---|
| 3.28 LTR | 3.9 | Identical API. RenameAttributes capability available since 3.0. |
| 3.34 LTR | 3.12 | Baseline for this page. |
| 3.40 / 3.44 | 3.12 | native:retainfields provides the whitelist operation directly, without index arithmetic. |
deleteAttributes(), renameAttributes() and native:deletecolumn are unchanged across 3.x. On 3.40 and newer, native:retainfields is the cleaner expression of the whitelist pattern.
Troubleshooting
- The wrong columns disappeared. Deletion ran in ascending index order. Always sort descending, and resolve every index before deleting any.
deleteAttributes()had no effect. One or more indexes were-1, or the provider lacks the capability. Check both explicitly.- A rename returned False. The new name collides with an existing field, or the provider does not support renaming. Shapefiles do not.
- Expressions broke after a rename. Styles, labels and joins reference field names and are stored in the project, so they do not follow. Use an alias instead when only the display label matters.
- Fields reappear after reopening the project. The deletion was staged in an edit buffer that was never committed. Either commit it or use the provider route.
- The layer still lists the old fields.
updateFields()was not called after the deletion.
Conclusion
Delete by name, resolve to indexes in one pass, and remove them in descending order — that single discipline prevents the whole class of "the wrong column vanished" failures. Prefer a whitelist to a blacklist so upstream schema changes are handled automatically, use aliases rather than renames when only the label matters, and reach for native:deletecolumn whenever the operation should be re-runnable.
Frequently Asked Questions
Why did deleting fields remove the wrong ones? Indexes are positional and shift as soon as one field is removed. Resolve every index against the original schema first, then delete them in descending order so each removal only affects indexes already handled.
Can I delete a field by name instead of index?
Not through the provider API, which takes indexes. native:deletecolumn does take names and writes a new layer, which is why it is the safer option in a pipeline.
Why can I not rename a field in my shapefile?
The shapefile driver does not support renaming attributes. Check capabilities() & QgsVectorDataProvider.RenameAttributes first, or convert the data to GeoPackage.
What is the difference between renaming a field and setting an alias? A rename changes the underlying column name and breaks every expression, style and join referring to the old one. An alias changes only the display label in the interface and is stored in the project, so nothing downstream is affected.