Filter and Sort Atlas Features in PyQGIS

An atlas makes one page per feature of its coverage layer — every district, every inspection area, every sheet of a map grid. In practice you rarely want every feature in whatever order the data source returns: the planning team wants only districts with open applications, sorted by priority; the field crew wants their own area's sheets in route order; the annual report wants districts alphabetically. The atlas has settings for exactly this — a filter expression, a sort expression and a page-name expression — and from Python they can be changed between runs to produce several tailored series from one layout.

This recipe belongs to Automating Atlas Map Series. It filters coverage features with an expression, sorts pages by fields and computed values, names pages for output files, hides the coverage layer, and runs the same atlas several times with different filters.

From coverage layer to page sequenceThe coverage layer holds all features. The filter expression keeps only those that should become pages, for example districts with open applications. The sort expression orders the remaining features, for example by priority descending then name. The page-name expression gives each page a name used for file names and the page number list. The result is the page sequence the atlas renders.Filter, sort, namecoverageall 48districtsfilter"open_apps" > 031 leftsort"priority" descthen namepages31, named

Prerequisites

  • QGIS 3.34 LTR or newer, or the QGIS 4 series.
  • A layout with an atlas enabled and a coverage layer set, created in the designer or in code as in configuring an atlas coverage layer.

Filter the coverage features

The filter expression is evaluated for each coverage feature; only those returning true become pages.

from qgis.core import QgsProject

layout = QgsProject.instance().layoutManager().layoutByName("District atlas")
atlas = layout.atlas()
coverage = atlas.coverageLayer()
print("coverage:", coverage.name(), coverage.featureCount(), "features")

atlas.setFilterFeatures(True)
ok, error = atlas.setFilterExpression('"open_apps" > 0 AND "status" <> \'archived\'')
if not ok:
    raise ValueError(f"bad filter: {error}")
atlas.updateFeatures()
print(atlas.count(), "pages after filtering")

Breakdown: setFilterFeatures(True) switches filtering on; the expression alone does nothing without it. setFilterExpression validates the expression and returns an error message for syntax mistakes, which is worth checking because a broken filter otherwise yields an atlas of zero pages. updateFeatures() re-evaluates the filter and sorting immediately; count() then reports the page count, a good sanity check before a long export. NULL values follow expression rules — a NULL open_apps is not greater than zero, so those districts are excluded; use coalesce if they should count.

Sort pages

Without sorting, page order is the provider's feature order. A sort expression makes it deliberate.

atlas.setSortFeatures(True)
atlas.setSortExpression('"priority"')
atlas.setSortAscending(False)
atlas.updateFeatures()

names = []
for i in range(atlas.count()):
    atlas.seekTo(i)
    names.append(layout.reportContext().feature()["name"])
print(names[:10])

Breakdown: Sorting by "priority" descending puts urgent districts first. The expression can be any expression, so composite orders are possible, as the next section shows. Seeking through the atlas and reading each current feature confirms the order before exporting. The report context holds the current atlas feature after seekTo.

Composite sort keysA single sort expression can encode several keys by building a sortable string or number: priority as a zero-padded number followed by the name, so pages are ordered by priority and alphabetically within each priority. For route order, a sort by a precomputed sequence field gives the order a field crew will travel.One expression, several keyspriority, then namelpad(10 - "priority", 2, '0')|| "name"ascendingroute order"route_seq"from a routing stepascending

Sort by more than one key

The atlas takes one sort expression, but one expression can combine keys by building a string that sorts correctly.

atlas.setSortExpression("lpad(to_string(10 - \"priority\"), 2, '0') || ' ' || \"name\"")
atlas.setSortAscending(True)
atlas.updateFeatures()

Breakdown: Turning priority 9 into "01" and priority 1 into "09" makes higher priorities sort first in ascending order; appending the name sorts alphabetically within each priority. Zero-padding is essential, because strings compare character by character — "10" sorts before "9" otherwise. For a field route, compute a sequence number with the nearest facility or shortest path tools and sort by it, so pages appear in the order the crew drives.

Name pages for output files

The page-name expression gives each page a name. It appears in the atlas toolbar's page list and, more usefully, can drive file names when exporting one file per page.

atlas.setPageNameExpression("\"district_code\" || '_' || replace(\"name\", ' ', '_')")
atlas.setFilenameExpression("'district_' || @atlas_pagename")
atlas.updateFeatures()

atlas.first()
print(atlas.currentFilename(), atlas.nameForPage(0))

Breakdown: A page name built from a stable code plus the name sorts sensibly in a file browser and survives renamed districts better than the name alone. The filename expression references @atlas_pagename, so both stay in step; replacing spaces avoids awkward file names. Exporting with these names is covered in exporting atlas pages to individual PDFs.

Build the filter from a selection

Interactive tools often let a user select features on the map and then print "just these". The atlas filter can be generated from the coverage layer's current selection.

selected = coverage.selectedFeatureIds()
if not selected:
    raise ValueError("select at least one feature to print")
id_list = ",".join(str(i) for i in selected)
atlas.setFilterFeatures(True)
atlas.setFilterExpression(f"$id IN ({id_list})")
atlas.updateFeatures()
print(atlas.count(), "pages from the selection")

Breakdown: Feature ids of the selection go straight into an IN list, which the expression engine evaluates quickly even for hundreds of ids. Refusing an empty selection prevents an accidental export of zero pages — or, if the filter were skipped, of every page. Within a plugin, wire this to a button so users can select, click and get their maps, as described in adding a toolbar button.

Preview the page list before exporting

A long atlas export can take an hour. Listing the pages it will produce — names, order and count — takes a second and catches filter and sort mistakes before the time is spent.

def preview_pages(atlas, limit=20):
    atlas.updateFeatures()
    rows = []
    for i in range(min(atlas.count(), limit)):
        rows.append((i + 1, atlas.nameForPage(i)))
    return atlas.count(), rows

total, rows = preview_pages(atlas)
print(total, "pages")
for n, name in rows:
    print(f"{n:>3}  {name}")

Breakdown: nameForPage returns each page's evaluated name without rendering anything, so the preview is instant. Printing the first twenty in order is usually enough to see whether filtering and sorting did what was intended; the total confirms the size of the run. Put this check at the top of export scripts and stop if the count is zero or wildly different from expectations.

Hide the coverage layer

The coverage layer is often a helper — a grid of map sheets, the district boundaries used only to drive pages. The atlas can hide it from every map item without changing the project's visibility.

atlas.setHideCoverage(True)
map_item = layout.itemById("main_map")
map_item.setAtlasDriven(True)
map_item.setAtlasScalingMode(map_item.Auto)
map_item.setAtlasMargin(0.1)

Breakdown: With setHideCoverage, the coverage layer is not drawn on any atlas page, whatever its visibility in the Layers panel. The map item must be atlas-driven to follow the current feature; Auto scaling chooses fixed or fitted scale depending on the feature's size, and a 10 % margin keeps the feature off the frame edge. To show the current feature differently — highlighted while others are dimmed — see highlighting the atlas feature.

Run one atlas several ways

Different audiences need different subsets of the same series. Changing the filter between exports produces each subset from one layout, without copies to maintain.

Several exports from one layoutOne layout with an atlas is exported three times. Each run sets a different filter and output folder: the north team's districts, the south team's districts, and all districts with open applications. The layout itself is never duplicated, so design changes apply to every run.Same design, different subsetsone layoutdesign onceteam northteam southopen applicationsfolder per runsame file names

from qgis.core import QgsLayoutExporter
from pathlib import Path

runs = {
    "team_north": "\"team\" = 'north'",
    "team_south": "\"team\" = 'south'",
    "open_applications": "\"open_apps\" > 0",
}
for run, expr in runs.items():
    atlas.setFilterFeatures(True)
    atlas.setFilterExpression(expr)
    atlas.updateFeatures()
    out_dir = Path("/data/maps") / run
    out_dir.mkdir(parents=True, exist_ok=True)
    result, error = QgsLayoutExporter.exportToPdfs(atlas, str(out_dir / "page.pdf"),
                                                   QgsLayoutExporter.PdfExportSettings())
    print(f"{run:<20} {atlas.count():>3} pages  {'ok' if result == QgsLayoutExporter.Success else error}")

Breakdown: Each run sets its filter, refreshes the feature list and exports into its own folder, using the filename expression for individual files. The layout is never duplicated, so a design change — a new logo, a corrected legend — applies to every audience's maps at once. Restore the original filter afterwards if the project will be saved, or the next person to open the atlas sees only the last subset.

QGIS version compatibility

QgsLayoutAtlas filter, sort, page name and filename settings work on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. QgsLayoutExporter.exportToPdfs for atlases has existed throughout QGIS 3. On QGIS 4, QgsLayoutItemMap.Auto is QgsLayoutItemMap.AtlasScalingMode.Auto.

Troubleshooting

  • The atlas has zero pages. The filter expression is invalid or excludes everything; check the return value of setFilterExpression and count().
  • Order looks random. Sorting was not enabled, or numbers are compared as text without padding.
  • The coverage boundaries still show. setHideCoverage is off, or the same data is loaded as a second layer.
  • The next user sees a subset. A filter from the last run was saved in the project; reset it.

Conclusion

Filter atlas pages with a validated expression and enable filtering explicitly, sort with an expression that encodes all keys with padding, name pages from stable codes for file names, hide helper coverage layers, and run one layout with different filters for different audiences instead of copying it.

Frequently Asked Questions

Can I filter on attributes of another layer? Yes, with aggregate() or get_feature() in the filter expression, at some performance cost.

Can the atlas skip features without geometry? Features without geometry cannot be framed; filter them out with $geometry IS NOT NULL.

Can pages be ordered by area? Sort by $area descending to put the largest features first.

How do I print only pages changed since the last run? Keep an edited_on field on the coverage layer and filter with "edited_on" >= to_date('2026-09-01'), using the date of the previous export.

Does filtering change the coverage layer? No. The filter applies only to the atlas.