PyQGIS – Adding and Deleting Fields in Shapefile

deletefields-attributespyqgisqgis-3shapefile

As part of a project, I have to extract X, Y coordinates from points in a shapefile, and classify each point as North or South, or East or West.

To do so, I created new fields:

  • a field to store the X coordinates
  • a field to store the Y coordinates
  • a field to store the North/South classification for each point based on the Y coordinates
  • a field to store the East/West classification for each point based on the X coordinates

I wrote code to add these new fields, and I wrote code to delete these new fields to make the script re-runnable. All fields are added just fine, but only some fields are deleted. If I run the script again, all the fields are added again as extras (i.e. XCoord [original]…XCoord1[extras]…XCoord2[extras], etc.), and again, only some fields are deleted, and so on.

This is must be a problem with my delete code, but I'm not sure where I've gone wrong exactly. Is it the placement of my code within the script, or syntax, or something else?

My current placement of addition and deletion code is as below, before and after my mainline, respectively. I have tried including it within the mainline, but to no avail. I have also tried adding the deletion code first, and then the addition code.

# import libraries
import os
from qgis.PyQt.QtCore import QVariant

# Make shapefile the working layer.
currLayer = qgis.utils.iface.activeLayer()

caps = currLayer.dataProvider().capabilities()
currLayer.updateFields()

# Add the coordinate and classification attributes.
if caps & QgsVectorDataProvider.AddAttributes:

# I put the attributes on new lines for readability on GIS stack exchange.
    res = currLayer.dataProvider().addAttributes([
                QgsField("XCoord", QVariant.String),
                QgsField("YCoord", QVariant.String),
                QgsField("NSHemi", QVariant.String)
                QgsField("EWHemi", QVariant.String)])

# Commit the addition of fields.
currLayer.updateFields()

############################################################
### Main line, which works just fine ###
############################################################

caps = currLayer.dataProvider().capabilities()
currLayer.updateFields()

# Delete the classification and coordinate attributes from the shapefile.
if caps & QgsVectorDataProvider.DeleteAttributes:

    # Get the field names from the current layer.
    fields = currLayer.fields()

    # Determine their index numbers.
    idxX = fields.indexFromName('XCoord')
    idxY = fields.indexFromName('YCoord')
    idxNSHemi = fields.indexFromName('NSHemi')
    idxEWHemi = fields.indexFromName('EWHemi')

    # If the index number is -1, that means the index search
    # didn't find the attribute. If index number is greater
    # than -1, the position is returned.

    if idxX > -1:
        res = currLayer.dataProvider().deleteAttributes([idxX])
    if idxY > -1:
        res = currLayer.dataProvider().deleteAttributes([idxY])
    if idxNSHemi > -1:
        res = currLayer.dataProvider().deleteAttributes([idxNSHemi])
    if idxEWHemi > -1:
        res = currLayer.dataProvider().deleteAttributes([idxEWHemi])

# Commit the removal of fields.
currLayer.updateFields()

Best Answer

The problem is very logical once you realise that field indexes are a dynamic thing. You are retrieving field indexes by their name in advance, then attempting to use those indexes when deleting fields one by one. The problem is that, immediately after the first field has been deleted, the indexes have dynamically changed, so the remaining previously calculated indexes are no longer valid!

So, if you want to delete attributes one by one, You will need to retrieve the field index from the field name at each step immediately before deletion, and also call updateFields() after each step.

*As a side note, I think you should use a decimal numeric type for X and Y fields.

currLayer = qgis.utils.iface.activeLayer()

caps = currLayer.dataProvider().capabilities()
currLayer.updateFields()

# Add the coordinate and classification attributes.
if caps & QgsVectorDataProvider.AddAttributes:

# I put the attributes on new lines for readability on GIS stack exchange.
    res = currLayer.dataProvider().addAttributes([
            QgsField("XCoord", QVariant.Double),
            QgsField("YCoord", QVariant.Double),
            QgsField("NSHemi", QVariant.String),
            QgsField("EWHemi", QVariant.String)])

# Commit the addition of fields.
currLayer.updateFields()

caps = currLayer.dataProvider().capabilities()

# Delete the classification and coordinate attributes from the shapefile.
if caps & QgsVectorDataProvider.DeleteAttributes:
    # If the index number is -1, that means the index search
    # didn't find the attribute. If index number is greater
    # than -1, the position is returned.
    idxX = currLayer.fields().indexFromName('XCoord')
    if idxX > -1:
        res = currLayer.dataProvider().deleteAttributes([idxX])
        print(res)# Check if operation returns True or False
        currLayer.updateFields()
    idxY = currLayer.fields().indexFromName('YCoord')
    if idxY > -1:
        res = currLayer.dataProvider().deleteAttributes([idxY])
        print(res)# Check if operation returns True or False
        currLayer.updateFields()
    idxNSHemi = currLayer.fields().indexFromName('NSHemi')
    if idxNSHemi > -1:
        res = currLayer.dataProvider().deleteAttributes([idxNSHemi])
        print(res)# Check if operation returns True or False
        currLayer.updateFields()
    idxEWHemi = currLayer.fields().indexFromName('EWHemi')
    if idxEWHemi > -1:
        res = currLayer.dataProvider().deleteAttributes([idxEWHemi])
        print(res)# Check if operation returns True or False
        currLayer.updateFields()

However, a much simpler way would be to pass a list of indexes to the deleteAttributes() method so that the fields will be deleted in one go.

E.g.

caps = currLayer.dataProvider().capabilities()

# Delete the classification and coordinate attributes from the shapefile.
if caps & QgsVectorDataProvider.DeleteAttributes:
    fld_names = ['XCoord', 'YCoord', 'NSHemi', 'EWHemi']
    indexes = [i for i in [currLayer.fields().lookupField(n) for n in fld_names] if i > -1]
    print(indexes)
    res = currLayer.dataProvider().deleteAttributes(indexes)
    print(res)# Check if operation returns True or False
    currLayer.updateFields()
Related Question