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.

One coverage feature, one pageThe coverage layer holds five ward polygons. The atlas iterates them in the configured order, and for each one the layout's map item recentres on that feature at the configured margin or scale. The result is five pages from one layout, with labels and titles that can reference the current feature's attributes.The layout is designed once; the atlas repeats itcoverage layerNorth wardRiverside wardCentral wardUpland wardfiltered and sorted by youthe layoutmap, title, legenddesigned oncenorth.pdf — framed on Northriverside.pdfcentral.pdfupland.pdfTitles and labels read the current feature, so each page describes itself

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.

Three ways to frame each pageAutomatic scaling fits every feature to the frame with a set margin, so pages vary in scale but each subject fills the page. Fixed scaling keeps one scale and recentres, so pages are directly comparable but small features look lost and large ones do not fit. Predefined scaling picks the nearest round scale from a list, giving variation with printable scale bars.Pick by whether pages must be comparableAutoeach feature fills the pagescale differs per pagegood for site plansFixedone scale throughoutpages compare directlylarge features overflowPredefinednearest scale from a listround numbers on the bargood for printed series

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.

From a field value to a file nameA ward name containing capitals, spaces and punctuation is transformed by the file name expression into lower case with spaces replaced, and a unique code appended. The result is a file name that is safe on every platform and cannot collide with another page, which matters because a colliding name means every page overwrites the same file.The expression output becomes a real file namethe field valueSt Mary's Old Townthe expressionlower, replace spaces, add codethe fileward st marys w04.pdfa NULL field yields an empty nameevery page then overwrites the same file, and 200 pages become one

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 versionPythonNotes
3.22 LTR3.9layout.atlas() with all properties shown.
3.28 LTR3.9Identical.
3.34 LTR3.12Baseline for this page.
3.40 / 3.443.12Identical 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

  • AttributeError on atlas(). layoutByName() returned None. 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() without endRender(). 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.