Show Features in an Attribute Table View in PyQGIS

Plugins often need to show features in a table: the results of a search, the parcels affected by a planned road, the assets due for inspection. A plain QTableWidget filled in a loop works for a dozen rows and becomes slow, disconnected from the layer and out of date as soon as anything is edited. QGIS's own attribute table — the one behind the Open Attribute Table dialog — is available as reusable classes: a model over the layer, a filter model that decides which rows show, and a view or dual view that displays them, with editing, sorting, field widgets and map selection sync built in.

This recipe belongs to Qt Designer for GIS Interfaces. It embeds an attribute table in a dock widget, filters it to selected or matching features, links its selection to the map, controls editing and columns, and uses the dual view for a table-and-form layout.

Model, filter, viewQgsVectorLayerCache caches features from the layer. QgsAttributeTableModel exposes them as rows and columns. QgsAttributeTableFilterModel decides which rows are shown — all, selected, visible on the map or matching an expression — and handles sorting. QgsAttributeTableView or QgsDualView displays the filtered model with QGIS's editor widgets and selection behaviour.The same classes as QGIS's own tablelayer cacheQgsVectorLayerCachetable modelrows = featuresfilter modelwhich rows, orderviewtable or dual

Prerequisites

  • QGIS 3.34 LTR or newer, or the QGIS 4 series, with code running inside QGIS Desktop (the classes need the map canvas for some filters).
  • A plugin with a dialog or dock widget, as in adding a custom dock widget.

Build the model and view

The stack is assembled in order: a feature cache over the layer, a table model over the cache, a filter model over the table model, and a view showing the filter model.

from qgis.core import QgsProject, QgsVectorLayerCache
from qgis.gui import QgsAttributeTableModel, QgsAttributeTableFilterModel, QgsAttributeTableView
from qgis.utils import iface

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

cache = QgsVectorLayerCache(layer, 10000)
model = QgsAttributeTableModel(cache)
model.loadLayer()
filter_model = QgsAttributeTableFilterModel(canvas, model)
view = QgsAttributeTableView()
view.setModel(filter_model)
view.show()

Breakdown: The cache size is the number of features held in memory; 10,000 covers most layers and larger layers still work, fetching as needed. loadLayer() populates the model. The filter model needs the map canvas because some filters depend on the visible extent. The view is a normal Qt widget: add it to a layout in your dock or dialog instead of calling show(). Keep references to the cache, model and filter model on your widget so Python does not garbage-collect them while the view uses them.

Show only selected or matching features

The filter model has modes that mirror the attribute table's toolbar filter: all features, selected features, features visible on the map, and features matching a filter expression.

Filter modesShowAll lists every feature. ShowSelected lists the layer's current selection and updates when it changes. ShowVisible lists features in the current map extent and updates on pan and zoom. ShowFilteredList lists a given set of feature ids, used with an expression filter, for example parcels with open applications.What the table showsShowAllevery featureShowSelectedfollowsselectionShowVisiblemap extentfollows panningShowFilteredListids froman expression

from qgis.core import QgsFeatureRequest, QgsExpression

filter_model.setFilterMode(QgsAttributeTableFilterModel.ShowSelected)

# or: only features matching an expression
expr = QgsExpression('"open_applications" > 0')
ids = [f.id() for f in layer.getFeatures(QgsFeatureRequest(expr).setNoAttributes()
                                          .setFlags(QgsFeatureRequest.NoGeometry))]
filter_model.setFilterMode(QgsAttributeTableFilterModel.ShowFilteredList)
filter_model.setFilteredFeatures(ids)
print(filter_model.rowCount(), "rows shown")

Breakdown: ShowSelected is the natural mode for a results panel that follows what the user selects on the map; it updates automatically. For search results, compute matching ids — with the request trimmed to no attributes and no geometry for speed — and pass them to setFilteredFeatures in filtered-list mode. Recent releases also accept a filter expression directly on the filter model, which keeps it current as features change; check setFilterExpression on your version. ShowVisible makes the table follow the map extent, useful for "what is on screen" panels.

The attribute table view shares the layer's selection: selecting rows selects features, and selecting on the map selects rows. Adding zoom-to and flash behaviour makes the panel feel native.

def zoom_to_selected_rows():
    ids = layer.selectedFeatureIds()
    if ids:
        canvas.zoomToFeatureIds(layer, ids)
        canvas.flashFeatureIds(layer, ids)

view.doubleClicked.connect(lambda index: zoom_to_selected_rows())
layer.selectionChanged.connect(lambda *_: print(len(layer.selectedFeatureIds()), "selected"))

Breakdown: Because the view works on the layer's own selection, nothing extra is needed for sync — that is a key advantage over a hand-built table. Double-clicking zooms to the selected features and flashes them briefly so the eye finds them. Disconnect signal handlers in the plugin's unload(); connecting layer signals explains the pattern.

Control columns and editing

Results panels usually show a few columns and are read-only; data-entry panels show more and allow edits. The layer's attribute table configuration controls columns, and the layer's edit mode controls editing.

config = layer.attributeTableConfig()
keep = {"parcel_id", "owner_name", "area_ha", "open_applications"}
columns = config.columns()
for c in columns:
    c.hidden = c.name not in keep
config.setColumns(columns)
filter_model.setAttributeTableConfig(config)

view.setEditTriggers(view.NoEditTriggers)          # read-only panel

Breakdown: The attribute table config lists columns with visibility, width and order; hiding all but the relevant fields keeps the panel focused. Applying the config to the filter model affects only this view, not the layer's own attribute table dialog — use layer.setAttributeTableConfig(config) if the change should apply everywhere. With edit triggers disabled, the view is read-only even when the layer is in edit mode; leave the default triggers for panels where users should edit values, which then use the layer's configured editor widgets.

Package it as a reusable results panel

Plugins that show results in several places benefit from one widget class that wraps the whole stack, with a single method to show a set of features. It keeps references alive, hides the plumbing and makes the panel easy to drop into any dialog or dock.

from qgis.PyQt.QtWidgets import QWidget, QVBoxLayout, QLabel

class ResultsTable(QWidget):
    def __init__(self, canvas, parent=None):
        super().__init__(parent)
        self.canvas = canvas
        self.cache = self.model = self.filter_model = None
        self.label = QLabel("No results")
        self.view = QgsAttributeTableView()
        self.view.setEditTriggers(self.view.NoEditTriggers)
        lay = QVBoxLayout(self)
        lay.addWidget(self.label)
        lay.addWidget(self.view)
        self.view.doubleClicked.connect(self._zoom)

    def show_features(self, layer, ids):
        self.layer = layer
        self.cache = QgsVectorLayerCache(layer, 5000)
        self.model = QgsAttributeTableModel(self.cache)
        self.model.loadLayer()
        self.filter_model = QgsAttributeTableFilterModel(self.canvas, self.model)
        self.filter_model.setFilterMode(QgsAttributeTableFilterModel.ShowFilteredList)
        self.filter_model.setFilteredFeatures(list(ids))
        self.view.setModel(self.filter_model)
        self.label.setText(f"{len(ids)} {layer.name()} features")

    def _zoom(self, index):
        ids = self.layer.selectedFeatureIds()
        if ids:
            self.canvas.zoomToFeatureIds(self.layer, ids)

Breakdown: Holding the cache, model and filter model as attributes of the widget ties their lifetime to the panel, which avoids the most common bug with these classes — an empty table because the model was garbage-collected. Rebuilding the stack in show_features lets the same panel show results from different layers over time. The label gives immediate feedback on how many features matched. Any search or analysis in the plugin can now finish with results.show_features(layer, ids).

Use the dual view for table and form

QgsDualView combines the table with a form: a list of features on the left and the attribute form for the current one on the right, or the full table — exactly the attribute table dialog's two modes.

The dual viewQgsDualView offers two modes. Table mode shows the attribute table. Form mode shows a feature list on the left and the layer's configured attribute form for the current feature on the right, including tabs, widgets and constraints. It is initialised with the layer, the canvas and a feature request that limits which features are loaded.Table mode, or list plus formtable modelike the attribute tableform modefeature list+ configured formtabs, widgets, constraints

from qgis.gui import QgsDualView, QgsAttributeEditorContext

dual = QgsDualView()
context = QgsAttributeEditorContext()
context.setMapCanvas(canvas)
request = QgsFeatureRequest().setFilterExpression('"open_applications" > 0')
dual.init(layer, canvas, request, context)
dual.setView(QgsDualView.AttributeEditor)          # form mode
dual.show()

Breakdown: init takes the layer, canvas, a feature request limiting which features load, and an editor context. The request is the simplest way to show a subset in the dual view. Form mode presents the layer's own form, so all the configuration done for data entry — tabs, drop-downs, constraints, form logic — appears in the plugin without extra work. setView(QgsDualView.AttributeTable) switches to table mode. The dual view is heavier than a bare table view; use it when users need to edit features one at a time.

QGIS version compatibility

QgsVectorLayerCache, QgsAttributeTableModel, QgsAttributeTableFilterModel, QgsAttributeTableView and QgsDualView are available in qgis.gui on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. Filter expressions on the filter model were added in the 3.x series. On QGIS 4, enums are scoped: QgsAttributeTableFilterModel.FilterMode.ShowSelected, QgsDualView.ViewMode.AttributeEditor.

Troubleshooting

  • The view is empty. loadLayer() was not called, or the model objects were garbage-collected; keep references.
  • The table does not follow the selection. The filter mode is ShowAll; set ShowSelected.
  • Column changes affect the main attribute table. The config was applied to the layer instead of the filter model.
  • Editing is impossible. The layer is not in edit mode, or edit triggers were disabled.

Conclusion

Embed QGIS's attribute table with a layer cache, table model, filter model and view, choose a filter mode — selected, visible or a filtered id list — for the panel's purpose, rely on the shared selection for map sync and add zoom and flash, control columns through the attribute table config on the filter model, make results panels read-only, and use QgsDualView when users edit through the layer's form.

Frequently Asked Questions

Can I show features from several layers in one table? Not with these classes; one model per layer. Use tabs or a combined memory layer.

Does the table update when features are edited elsewhere? Yes — the cache and model listen to layer signals.

Can I add a column computed in Python? Add a virtual field with an expression to the layer; it appears like any other column.

Can users export the table? Select rows and copy them with Ctrl+C, which copies attributes as tab-separated text; for files, export the filtered features with Processing.

Is it fast for large layers? The cache loads features on demand, so even large layers open quickly; filtered lists keep panels small.