Configure an Atlas Coverage Layer in PyQGIS
An atlas turns one layout into one map per feature: a page per ward, per site, per survey square. The layout work is cartography, but the configuration — which layer drives it, which features are included, in what order, at what scale, saved under what name — is a dozen properties, and setting them from Python is what makes an atlas a scheduled job rather than a click-through.
This recipe belongs to Automating Atlas Map Series. It covers pointing an atlas at a coverage layer, filtering and sorting the pages, controlling how each page frames its feature, naming the output files from an expression, and checking a page before committing to a two-hundred-page export.
Prerequisites
- QGIS 3.34 LTR (bundled Python 3.12) or newer.
- A project containing a layout with at least one map item, and a polygon or point layer to drive the atlas.
- Familiarity with layouts — Automated Map Layout Generation covers building one from scratch.
Point the atlas at a coverage layer
from qgis.core import QgsProject
project = QgsProject.instance()
layout = project.layoutManager().layoutByName("Ward maps")
wards = project.mapLayersByName("Wards")[0]
atlas = layout.atlas()
atlas.setEnabled(True)
atlas.setCoverageLayer(wards)
atlas.setHideCoverage(False)
print(atlas.count(), "pages")
Breakdown: layoutByName() returns None when the name does not match exactly, and the resulting AttributeError on .atlas() is the usual first error — print the names from layoutManager().printLayouts() if in doubt. setEnabled(True) is what turns the layout into an atlas; without it every other property is set and ignored. setHideCoverage() controls whether the driving layer is drawn on the map, which is usually wanted for ward boundaries and usually not for an invisible grid of survey squares. count() reports how many pages the current configuration produces, and checking it before exporting is the cheapest possible sanity check.
Filter and sort the pages
atlas.setFilterFeatures(True)
atlas.setFilterExpression("\"status\" = 'active' AND \"population\" > 500")
atlas.setSortFeatures(True)
atlas.setSortExpression("\"ward_name\"")
atlas.setSortAscending(True)
print(atlas.count(), "pages after filtering")
Breakdown: The filter is a QGIS expression evaluated against the coverage layer, with field names in double quotes and string literals in single — the usual quoting trap. Both the filter and the sort need their set...Features(True) companion, and setting the expression without enabling it is a silent no-op that produces a full, unsorted atlas. Sorting matters more than it looks in a printed document: pages in alphabetical order can be found by a reader, pages in feature-id order cannot. Re-checking count() after filtering confirms the expression did what you meant rather than excluding everything.
Control how each page frames its feature
Framing is a property of the map item, not the atlas, which is the part that most often confuses people.
from qgis.core import QgsLayoutItemMap
map_item = next(item for item in layout.items() if isinstance(item, QgsLayoutItemMap))
map_item.setAtlasDriven(True)
map_item.setAtlasScalingMode(QgsLayoutItemMap.Auto)
map_item.setAtlasMargin(0.10) # 10 percent around the feature
Breakdown: setAtlasDriven(True) is what makes the map follow the atlas feature at all; a layout with an atlas configured but no atlas-driven map produces identical pages showing the same extent. The scaling mode decides the framing rule: Auto fits each feature with the given margin, so a large ward and a small one are both filled sensibly at different scales; Fixed keeps the layout's scale and simply recentres, which is what you want when pages must be comparable; Predefined picks the nearest scale from the project's list, which gives varying zoom with round-number scales for a printed series. Choosing between them is a cartographic decision, and the wrong one shows up immediately as pages where the subject is a dot or overflows the frame.
Name the output files from the data
atlas.setFilenameExpression("'ward_' || lower(replace(\"ward_name\", ' ', '_'))")
Breakdown: The file name expression is evaluated per page against the coverage feature, so the output becomes ward_north.pdf, ward_riverside.pdf and so on rather than a folder of numbered files nobody can navigate. Normalising the value — lower case, spaces replaced — matters because the result becomes a real file name: a ward called "St Mary's / Old Town" produces a path that fails on Windows and confuses everything else. No extension is included; the exporter appends it. Where the coverage layer has a genuinely unique code, use it and put the readable name in the page title instead, which sidesteps the sanitising question entirely.
Preview one page before exporting all of them
atlas.beginRender()
atlas.first()
print(atlas.currentFeatureNumber(), atlas.nameForPage(0))
atlas.seekTo(3)
layout.refresh()
# inspect the layout here, or export this single page
atlas.endRender()
Breakdown: beginRender() puts the atlas into iteration mode, first() and next() step through it, and seekTo() jumps to a page by index — which is how you check page 4 of 200 without exporting the other 199. Every beginRender() needs its endRender(), or the layout stays in atlas mode and subsequent operations behave oddly. Refreshing after seeking is what updates the map item and any label expressions. Rendering a single page this way — with the layout exporter, as described in Export an Atlas to PDF in PyQGIS — turns a slow feedback loop into a fast one while you are getting the framing right.
Save the configuration into the project
Everything set here is stored in the layout, so writing the project persists the whole atlas configuration:
project.write()
Breakdown: That is the point of configuring an atlas from Python rather than exporting from a script every time: the project becomes a reusable artefact a cartographer can open, check and adjust, and the scheduled export is then a two-line job that opens the project and exports. It also means a change to the filter or the sort is a change to one file rather than to a script somebody has to find. The project-level side of this is covered in Working with QGIS Projects in PyQGIS.
QGIS version compatibility
| QGIS version | Python | Notes |
|---|---|---|
| 3.22 LTR | 3.9 | layout.atlas() with all properties shown. |
| 3.28 LTR | 3.9 | Identical. |
| 3.34 LTR | 3.12 | Baseline for this page. |
| 3.40 / 3.44 | 3.12 | Identical API; the atlas panel gained interface refinements only. |
QGIS 2's QgsAtlasComposition no longer exists; any snippet mentioning composition is pre-3.0 and needs rewriting against the layout API rather than patching.
Troubleshooting
AttributeErroronatlas().layoutByName()returnedNone. Check the exact layout name.- Every page shows the same extent. The map item is not atlas-driven. Set
setAtlasDriven(True). - The filter is ignored.
setFilterFeatures(True)was not called, or the expression has quoting reversed. count()is zero. The filter excludes everything, or the coverage layer has no features.- File names collide and pages overwrite each other. The name expression is not unique. Include a code, or fall back to the feature id.
- The layout behaves strangely after a script.
beginRender()withoutendRender(). Always pair them.
Conclusion
Enable the atlas, point it at a coverage layer, and check count(). Filter and sort with expressions — remembering that each needs its enabling call — and set the framing on the map item with the scaling mode that matches whether pages must be comparable. Drive file names from an expression that produces safe, unique names, preview a page with seekTo() before exporting hundreds, and save the project so the configuration is a reusable artefact.
Frequently Asked Questions
Can the coverage layer be points rather than polygons?
Yes. With Auto scaling a point has no extent to fit, so use Fixed or Predefined scaling, or buffer the points into a coverage layer of areas.
How do I put the current feature's name in the title?
Use a label with an expression such as [% "ward_name" %]; inside an atlas, expressions evaluate against the current feature automatically.
Can one project hold several atlases? Yes — one per layout, each with its own coverage layer and configuration.
How do I exclude features without editing the layer? The filter expression. It is evaluated per feature and changes nothing in the data.
Does the coverage layer have to be visible in the map?
No. setHideCoverage(True) keeps it driving the atlas without drawing it, which is right for an invisible grid.
How do I add a page number to each sheet?
Use the @atlas_featurenumber and @atlas_totalfeatures variables in a label expression. They are populated during iteration, so "Sheet 4 of 42" needs no extra configuration.
Can other layers react to the current atlas feature?
Yes — a layer filter or a rule-based style can reference @atlas_feature and its attributes, which is how a series highlights the current ward while still drawing its neighbours in grey.
Can I sort by something that is not a field?
Yes — the sort expression is a full QGIS expression, so length("ward_name") or a computed value works as well as a field.