Automating Atlas Map Series in PyQGIS
An atlas turns a single print layout into a series of maps — one page per feature in a coverage layer. Instead of designing forty district maps by hand, you design one and let QGIS iterate. PyQGIS takes this further: you configure and export the entire series from code, with no dialogs, so map books regenerate automatically whenever the underlying data changes. Within Spatial Data Processing & Automation with PyQGIS, atlas automation is the publishing endpoint — the step that turns processed data into a stack of finished, paginated maps.
This guide covers the full atlas object model in PyQGIS: attaching an atlas to a QgsLayout, setting the coverage layer, naming and filtering pages, controlling sort order, governing per-page extent, and exporting the series to a single PDF, one PDF per feature, or a folder of images. It is the conceptual companion to the focused recipe on generating an atlas PDF in PyQGIS.
Prerequisites
- QGIS 3.34 LTR (Python 3.12) recommended; the API is stable on 3.28 LTR and the 3.40/3.44 line.
- A QGIS project (
.qgz/.qgs) containing a print layout with at least one map item, plus a coverage layer — the polygon (or point) layer whose features become atlas pages. - The map item set to "controlled by atlas" so its extent follows each coverage feature.
- Comfort with
QgsProjectand the layout classes from Automated Map Layout Generation with PyQGIS, which this cluster extends from single layouts to multi-page series.
The Atlas Iteration Loop
Conceptually an atlas is a loop. QGIS reads the coverage layer, applies a filter and a sort, and for each surviving feature it sets the controlled map's extent, evaluates page-level expressions (titles, page names), renders the layout, and emits output. Understanding this loop is what lets you configure each stage correctly from code.
Accessing the Atlas Object
Every QgsPrintLayout owns a QgsLayoutAtlas, reachable with layout.atlas(). From there you enable it and bind a coverage layer. This is the foundation every later configuration step builds on.
from qgis.core import QgsProject
project = QgsProject.instance()
layout = project.layoutManager().layoutByName("DistrictMaps")
if layout is None:
raise ValueError("Layout 'DistrictMaps' not found in project")
atlas = layout.atlas()
atlas.setEnabled(True)
coverage = project.mapLayersByName("districts")[0]
atlas.setCoverageLayer(coverage)
print(f"Atlas enabled with {coverage.featureCount()} candidate pages")
Breakdown: layoutManager().layoutByName(...) returns the named print layout or None, so the guard matters. layout.atlas() returns the layout's single QgsLayoutAtlas; setEnabled(True) activates iteration. setCoverageLayer(coverage) tells the atlas which layer's features become pages — at this point page count equals the coverage feature count, before any filter is applied. The coverage layer must already be loaded in the project, hence mapLayersByName.
Filtering and Sorting Pages
You rarely want every feature. setFilterFeatures(True) plus setFilterExpression(...) restricts the series to features matching a QGIS expression, and the sort settings control page order — essential for a logical map book.
# Only include districts flagged active, ordered alphabetically by name
atlas.setFilterFeatures(True)
ok = atlas.setFilterExpression('"status" = \'active\' AND "population" > 1000')
if not atlas.filterExpression() or not ok:
print("Filter expression invalid:", atlas.filterExpression())
atlas.setSortFeatures(True)
atlas.setSortExpression('"district_name"')
atlas.setSortAscending(True)
atlas.updateFeatures() # recompute the page list after changing filter/sort
print(f"Series now has {atlas.count()} pages")
Breakdown: setFilterFeatures(True) switches filtering on; setFilterExpression(...) accepts any valid QGIS expression referencing coverage-layer fields. In newer QGIS the setter returns a bool indicating a valid expression — always confirm, because an invalid filter silently yields zero pages. setSortFeatures / setSortExpression / setSortAscending define order. Crucially, updateFeatures() recomputes the filtered, sorted page list; without it count() may report stale numbers. Use atlas.count() to verify you have the pages you expect before exporting.
Naming Pages and Driving Dynamic Content
The pageNameExpression gives each page a meaningful name, used for per-feature output filenames and accessible to layout labels. Combined with the @atlas_* expression variables, this is how titles and stats update per page.
# Page name like "03_Riverside" — zero-padded index plus district name
atlas.setPageNameExpression(
"lpad(@atlas_featurenumber, 2, '0') || '_' || \"district_name\""
)
Breakdown: setPageNameExpression(...) is evaluated once per feature. Here @atlas_featurenumber is the 1-based position in the (filtered, sorted) series, lpad(...) zero-pads it so files sort correctly, and "district_name" pulls the field value. When you export "one file per feature", this expression becomes each filename. Layout text items can use the same variables — [% "district_name" %] in a label updates automatically as the atlas iterates. For dynamic styling per page (for example a choropleth that re-classifies per district), pair this with the renderer techniques in creating a choropleth map in PyQGIS.
Controlling Per-Page Extent
The map item flagged "controlled by atlas" reframes to each coverage feature. You govern the framing with margin or scale settings on that QgsLayoutItemMap.
from qgis.core import QgsLayoutItemMap
# Find the atlas-controlled map item in the layout
map_item = next(
item for item in layout.items()
if isinstance(item, QgsLayoutItemMap) and item.atlasDriven()
)
# Frame each feature with a 15% margin around its geometry
map_item.setAtlasScalingMode(QgsLayoutItemMap.AtlasScalingMode.Auto)
map_item.setAtlasMargin(0.15)
Breakdown: layout.items() returns every layout element; filtering for a QgsLayoutItemMap whose atlasDriven() is True finds the map that follows the atlas. AtlasScalingMode.Auto recomputes the scale per feature so the geometry fits; setAtlasMargin(0.15) adds 15% padding so features are not flush against the frame. The alternatives are Fixed (constant scale, frame centres on each feature) and Predefined (snaps to a configured scale list) — pick based on whether consistent scale or consistent framing matters more for your map book.
Exporting the Whole Series
With the atlas configured, QgsLayoutExporter exports the series. exportToPdf (atlas overload) can produce one combined PDF or one PDF per feature; exportToImage writes a folder of rasters.
import os
from qgis.core import QgsLayoutExporter
out_dir = "/data/atlas_out"
os.makedirs(out_dir, exist_ok=True)
exporter = QgsLayoutExporter(layout)
pdf_settings = QgsLayoutExporter.PdfExportSettings()
pdf_settings.dpi = 300
pdf_settings.forceVectorOutput = True
# A) One combined multi-page PDF
result, error = exporter.exportToPdf(
atlas, os.path.join(out_dir, "district_atlas.pdf"), pdf_settings
)
# B) One PDF per feature (filenames from pageNameExpression)
# result, error = exporter.exportToPdfs(
# atlas, os.path.join(out_dir, "districts"), pdf_settings
# )
# C) One PNG per feature
# img_settings = QgsLayoutExporter.ImageExportSettings()
# img_settings.dpi = 200
# result, error = exporter.exportToImage(
# atlas, os.path.join(out_dir, "districts"), "png", img_settings
# )
if result == QgsLayoutExporter.Success:
print("Atlas exported successfully")
else:
print("Export failed:", error)
Breakdown: The atlas-aware overloads take the atlas object as their first argument, which is what makes them iterate instead of rendering a single page. exportToPdf(atlas, ...) concatenates all pages into one document; exportToPdfs(atlas, base, ...) writes a separate file per feature named from the pageNameExpression. exportToImage(atlas, base, "png", ...) does the same for rasters. The atlas overloads return a (result, error) tuple — unlike the single-layout exportToPdf, which returns only a code — so unpack two values and check result == QgsLayoutExporter.Success. For a complete standalone script around option A, see generating an atlas PDF in PyQGIS. The same exporter underpins exporting multiple QGIS layouts to PDF, the non-atlas case.
Building a Layout and Atlas Entirely from Code
Most workflows start from a layout designed in the GUI, but you can construct the whole layout and its atlas programmatically — useful for templated map books generated on demand. The key is creating a QgsLayoutItemMap, flagging it atlas-driven, then wiring the atlas to it.
from qgis.core import (
QgsProject, QgsPrintLayout, QgsLayoutItemMap,
QgsLayoutPoint, QgsLayoutSize, QgsUnitTypes,
)
project = QgsProject.instance()
coverage = project.mapLayersByName("districts")[0]
layout = QgsPrintLayout(project)
layout.initializeDefaults()
layout.setName("GeneratedAtlas")
project.layoutManager().addLayout(layout)
# Atlas-controlled map item
map_item = QgsLayoutItemMap(layout)
map_item.attemptMove(QgsLayoutPoint(10, 10, QgsUnitTypes.LayoutMillimeters))
map_item.attemptResize(QgsLayoutSize(190, 250, QgsUnitTypes.LayoutMillimeters))
map_item.setLayers(list(project.mapLayers().values()))
map_item.setAtlasDriven(True)
map_item.setAtlasScalingMode(QgsLayoutItemMap.AtlasScalingMode.Auto)
map_item.setAtlasMargin(0.1)
layout.addLayoutItem(map_item)
# Wire the atlas
atlas = layout.atlas()
atlas.setEnabled(True)
atlas.setCoverageLayer(coverage)
atlas.updateFeatures()
print(f"Generated atlas with {atlas.count()} pages")
Breakdown: QgsPrintLayout(project) plus initializeDefaults() creates a blank A4 layout; addLayout registers it so the layout manager (and later export) can find it. The QgsLayoutItemMap is positioned and sized in millimetres, then setAtlasDriven(True) is the switch that makes this map follow the atlas — without it the atlas iterates but the map never reframes. setAtlasScalingMode and setAtlasMargin govern per-page framing exactly as on a GUI-built layout. This mirrors the single-layout construction in Automated Map Layout Generation with PyQGIS, adding the atlas wiring on top.
Compatibility Notes
| QGIS / Python | Atlas API notes |
|---|---|
| 3.28 LTR / Py 3.9 | QgsLayoutAtlas API stable. Enum access via flat names (e.g. QgsLayoutItemMap.Auto) works. setFilterExpression may not return a bool. |
| 3.34 LTR / Py 3.12 (baseline) | Recommended. Scoped enums (AtlasScalingMode.Auto) available; atlas export overloads return (result, error). |
| 3.40 / 3.44 / Py 3.12 | Same model. Minor expression-function additions; re-verify any expression-driven settings. |
Across versions, always call atlas.updateFeatures() after changing the coverage layer, filter, or sort, and always verify atlas.count() before a long export so you do not render an empty or unexpectedly large series.
Key Takeaways
- An atlas is a
QgsLayoutAtlasowned by aQgsPrintLayout; reach it vialayout.atlas(), enable it, and bind a coverage layer. - Filtering and sorting shape the series — call
updateFeatures()afterward and checkatlas.count()to confirm the page list. pageNameExpression(with@atlas_featurenumberand field references) names per-feature outputs and feeds dynamic labels.- Per-page framing is controlled on the atlas-driven
QgsLayoutItemMapviasetAtlasScalingModeandsetAtlasMargin. - The atlas overloads of
QgsLayoutExporter(exportToPdf,exportToPdfs,exportToImage) iterate the series and return a(result, error)tuple.
Frequently Asked Questions
How do I get the atlas object from a layout?
Call layout.atlas() on a QgsPrintLayout. Each print layout owns exactly one QgsLayoutAtlas. Enable it with setEnabled(True) and bind the coverage layer with setCoverageLayer(...).
Why does my atlas export zero pages?
Usually an invalid or over-restrictive filter, or you forgot updateFeatures() after changing the filter. Check atlas.filterExpression() is valid and inspect atlas.count() before exporting. A geographic mismatch between coverage and map CRS can also exclude everything.
What is the difference between exportToPdf and exportToPdfs?exportToPdf(atlas, path, settings) writes one combined multi-page PDF. exportToPdfs(atlas, base, settings) writes one PDF per feature, named from pageNameExpression. Both take the atlas object so they iterate the series.
How do I make titles and labels change per page?
Use expression-based layout labels referencing coverage fields, e.g. [% "district_name" %], plus atlas variables like @atlas_featurenumber. The atlas re-evaluates them for each feature during iteration.
Can I run an atlas export headlessly on a server?
Yes. Bootstrap QgsApplication, read the project with QgsProject.instance().read(...), configure the atlas, and call the atlas exporter overloads. The dedicated long-tail guide shows the full standalone script.