Rename and Reorder Fields in PyQGIS
Field names come from wherever the data was made: shapefile names truncated to ten characters (POPULATIO, AREA_HA_CA), exports with machine-generated prefixes, spreadsheets with spaces and capitals. Field order is whatever the last tool left. Before data is shared, joined or published, its schema usually needs tidying — clear names, a sensible order, no leftover columns — and when data must match a specification, the schema must be mapped exactly.
This recipe belongs to Attribute Tables & Field Management. It renames fields in place, rebuilds a whole schema with native:refactorfields, reorders and drops columns, sets aliases for display, and maps a supplier's schema onto a target specification.
Prerequisites
- QGIS 3.34 LTR or newer, or the QGIS 4 series.
- A writable layer for in-place renames — GeoPackage, PostGIS or a memory layer. Shapefiles support renames, but names stay limited to ten characters.
Rename a field in place
For a quick rename on an existing table, the layer's renameAttribute changes the name through the edit buffer, and the provider applies it on commit.
from qgis.core import QgsProject, edit
parcels = QgsProject.instance().mapLayersByName("parcels")[0]
renames = {"AREA_HA_CA": "area_ha", "OWNR_NM": "owner_name", "LST_UPD": "last_updated"}
with edit(parcels):
for old, new in renames.items():
idx = parcels.fields().indexOf(old)
if idx < 0:
print("no field", old)
continue
if not parcels.renameAttribute(idx, new):
raise RuntimeError(f"could not rename {old} → {new}")
print(parcels.fields().names())
Breakdown: Looking up each index by name, rather than assuming positions, keeps the script correct if fields have been added or reordered. renameAttribute returns False when the provider cannot rename — some formats and read-only sources — so check it. Inside edit(), a failure rolls back every rename, leaving the table unchanged. Renaming breaks anything that refers to the old name: expressions in styles and labels, joins, forms and other scripts. Search the project for the old names before renaming a widely used layer.
Rebuild the schema with refactor fields
For more than a few renames — or when order, types and dropped columns matter too — native:refactorfields builds a new layer from a complete field mapping. Each output field is defined by name, type, length, precision and an expression that fills it.
import processing
from qgis.PyQt.QtCore import QVariant
mapping = [
{"name": "parcel_id", "type": QVariant.String, "length": 20, "precision": 0, "expression": '"PRCL_ID"'},
{"name": "owner_name", "type": QVariant.String, "length": 120, "precision": 0, "expression": 'title("OWNR_NM")'},
{"name": "area_ha", "type": QVariant.Double, "length": 12, "precision": 4, "expression": "$area / 10000"},
{"name": "land_use", "type": QVariant.String, "length": 30, "precision": 0, "expression": 'lower("LU_CODE")'},
{"name": "last_updated", "type": QVariant.Date, "length": 0, "precision": 0, "expression": 'to_date("LST_UPD")'},
]
clean = processing.run("native:refactorfields", {
"INPUT": parcels, "FIELDS_MAPPING": mapping, "OUTPUT": "memory:parcels_clean"})["OUTPUT"]
print(clean.fields().names())
Breakdown: The output has exactly the fields in the mapping, in that order; anything not listed is dropped. Because each field is filled by an expression, renaming, recasing text, converting text dates to real dates and recomputing area all happen in the same step. The source layer is untouched, so the mapping can be refined and re-run. Field types are given as QVariant types on QGIS 3; on recent releases the mapping also accepts a sub_type key for list fields and QGIS 4 uses QMetaType values.
Reorder and drop columns quickly
When only order and selection change, generating the mapping from the existing fields saves typing.
def mapping_for(layer, order, rename=None):
rename = rename or {}
fields = layer.fields()
out = []
for name in order:
f = fields.field(name)
out.append({"name": rename.get(name, name), "type": f.type(), "length": f.length(),
"precision": f.precision(), "expression": f'"{name}"'})
return out
ordered = processing.run("native:refactorfields", {
"INPUT": clean,
"FIELDS_MAPPING": mapping_for(clean, ["parcel_id", "land_use", "area_ha", "owner_name"]),
"OUTPUT": "memory:parcels_ordered"})["OUTPUT"]
Breakdown: Reading type, length and precision from the existing fields preserves them exactly; only the order and the selection change. Fields left out of order are dropped. Putting identifiers first and long free-text columns last makes the attribute table and exported CSVs easier to read. For deleting a few fields from an existing table without rewriting it, deleting fields is simpler.
Normalise names automatically
Spreadsheet and shapefile exports often have names that are awkward in expressions and SQL: spaces, capitals, accents, punctuation, leading digits. A small normaliser turns every name into lower-case snake_case and makes duplicates unique, which is a sensible default before any further work.
import re
import unicodedata
def normalise(name):
s = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode()
s = re.sub(r"[^0-9a-zA-Z]+", "_", s).strip("_").lower()
return s if s and not s[0].isdigit() else f"f_{s}"
def normalised_mapping(layer):
seen, out = {}, []
for f in layer.fields():
new = normalise(f.name())
seen[new] = seen.get(new, 0) + 1
if seen[new] > 1:
new = f"{new}_{seen[new]}"
out.append({"name": new, "type": f.type(), "length": f.length(),
"precision": f.precision(), "expression": f'"{f.name()}"'})
return out
for m in normalised_mapping(parcels)[:6]:
print(m["expression"], "→", m["name"])
Breakdown: Unicode normalisation strips accents so Fläche becomes flache; any run of non-alphanumeric characters becomes one underscore; names starting with a digit get a prefix because many databases reject them. Counting repeats makes colliding names unique instead of letting the second silently overwrite the first. The function returns a ready field mapping, so the whole cleanup is one native:refactorfields call — and printing old-to-new pairs first lets you review the result before committing to it.
Set aliases for readable forms and tables
Sometimes the stored names must stay as they are — a database schema shared with other systems — but people should see friendly labels. Aliases change what QGIS displays without touching the data.
aliases = {"parcel_id": "Parcel ID", "owner_name": "Owner", "area_ha": "Area (ha)",
"land_use": "Land use", "last_updated": "Last updated"}
for name, alias in aliases.items():
idx = ordered.fields().indexOf(name)
if idx >= 0:
ordered.setFieldAlias(idx, alias)
print([ordered.attributeDisplayName(i) for i in range(ordered.fields().count())])
Breakdown: Aliases appear in the attribute table header, forms, identify results and the expression builder, while expressions and exports still use the real names. They are stored in the layer's style and project, so saving a QML keeps them. Aliases are the right tool for display; renames are the right tool when the data itself will be read by other people or systems.
Map a supplier schema onto a specification
Deliveries from contractors often arrive with their own field names. A mapping saved once and reused for every delivery converts each into the agreed schema and reports anything that does not fit.
import json
spec = json.load(open("/data/specs/parcels_v3.json")) # [{"name", "type", "length", "from"}]
type_map = {"string": QVariant.String, "double": QVariant.Double,
"int": QVariant.LongLong, "date": QVariant.Date}
delivery = QgsVectorLayer("/data/inbox/contractor_parcels.gpkg", "delivery", "ogr")
names = set(delivery.fields().names())
missing = [s["name"] for s in spec if s["from"] not in names]
unmapped = sorted(names - {s["from"] for s in spec})
mapping = [{"name": s["name"], "type": type_map[s["type"]], "length": s.get("length", 0),
"precision": s.get("precision", 0),
"expression": f'"{s["from"]}"' if s["from"] in names else "NULL"} for s in spec]
conformed = processing.run("native:refactorfields", {
"INPUT": delivery, "FIELDS_MAPPING": mapping, "OUTPUT": "memory:"})["OUTPUT"]
print("missing:", missing, "| unmapped:", unmapped)
Breakdown: Keeping the specification and the supplier mapping in a JSON file makes the conversion data, not code — update the file when the specification or the supplier changes. Missing source fields are filled with NULL so the output always has the full schema, and are reported so someone can chase them. Unmapped fields are listed rather than silently dropped, which often reveals useful columns the specification should adopt. Add from qgis.core import QgsVectorLayer when running this alone.
QGIS version compatibility
renameAttribute, field aliases and native:refactorfields work on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. The field mapping format with name, type, length, precision and expression keys is stable; recent releases add sub_type, type_name, alias and comment keys. On QGIS 4, types are QMetaType.Type values such as QMetaType.Type.QString.
Troubleshooting
renameAttributereturns False. The provider does not support renaming, or the layer is read-only; use refactor fields.- Styles and labels broke after renaming. They referenced the old names; update expressions or use aliases instead.
- Refactor produced NULL columns. The expression references a misspelled source field.
- Shapefile names are still truncated. Shapefiles limit names to ten characters; write to GeoPackage.
Conclusion
Rename a few fields in place with renameAttribute inside edit(), rebuild whole schemas with native:refactorfields where each field is named, typed and filled by an expression, reorder and drop by generating mappings from existing fields, use aliases when stored names must stay, and keep supplier-to-specification mappings in a file reused for every delivery.
Frequently Asked Questions
Does refactoring keep feature ids? In memory and GeoPackage outputs, new ids are assigned. Keep a business key field to link back to the source.
Can I rename fields in PostGIS?
Yes, through renameAttribute or with ALTER TABLE … RENAME COLUMN via the connections API.
Do aliases export to other formats? Only to formats that store them, such as GeoPackage with QGIS metadata; most formats keep the real names.
Can I add a computed field while refactoring? Yes — any expression works, including geometry functions and constants.