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.
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.
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.
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
setFilterExpressionandcount(). - Order looks random. Sorting was not enabled, or numbers are compared as text without padding.
- The coverage boundaries still show.
setHideCoverageis 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.