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 guide 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.
Previewing and Stepping Through Pages
Before committing to a long export you often want to inspect a single page — to confirm the extent, the page name, and any expression-driven labels look right. The atlas exposes a small navigation API that seeks to one feature at a time and updates the controlled map, without rendering anything to disk.
# Enter preview mode and walk the series feature by feature
atlas.beginRender() # prepares the atlas for iteration
atlas.first() # jump to the first page in the filtered/sorted series
print("Page 1:", atlas.currentFeatureNumber() + 1, "/", atlas.count())
print("Page name:", atlas.nameForPage(atlas.currentFeatureNumber()))
atlas.next() # advance one page; returns False past the end
atlas.seekTo(4) # jump straight to the 5th page (0-based index)
feature = atlas.currentFeature() # the coverage feature backing this page
print("Now on:", feature["district_name"])
atlas.endRender() # leave preview mode and restore the layout
Breakdown: beginRender() primes the atlas so first(), next(), previous(), last(), and seekTo(i) can move the current page; each move reframes the atlas-driven QgsLayoutItemMap and re-evaluates page expressions exactly as a real export would. currentFeatureNumber() is the 0-based index into the filtered, sorted series (add 1 for a human-facing page number), and currentFeature() returns the underlying QgsFeature so you can assert on its attributes in a test. Always pair beginRender() with endRender() — leaving the atlas mid-render can strand the layout on the last previewed feature. This navigation API is also how you drive a live map canvas or a Qt preview widget when building an interactive map-book tool.
Data-Defined Output Paths and Per-Page Overrides
For a real map book you rarely want a flat folder of files named only by district. The atlas honours the layout's data-defined output filename expression, so you can bucket pages into subfolders (by region, by status) and stamp the export date into each name — all evaluated per feature during exportToPdfs/exportToImage.
# Group output by region, then name each file by page number + district
atlas.setFilenameExpression(
"'/data/atlas_out/' || \"region\" || '/' "
"|| lpad(@atlas_featurenumber, 2, '0') || '_' || \"district_name\""
)
if not atlas.filenameExpression():
print("Filename expression rejected — falling back to page name")
# Restrict the export to one region without touching the master filter
region_atlas_filter = '"region" = \'North\''
atlas.setFilterExpression(region_atlas_filter)
atlas.updateFeatures()
print(f"Exporting {atlas.count()} North-region pages")
Breakdown: setFilenameExpression(...) overrides the plain pageNameExpression for file output — its result is the full path stem (without extension) for each per-feature file, so building it from "region" sorts pages into subdirectories automatically. QGIS creates the intermediate folders as needed, but the parent (/data/atlas_out/) must exist. Because the expression is evaluated inside the iteration loop, every @atlas_* variable and coverage field is in scope. Re-running setFilterExpression plus updateFeatures() lets a single script emit several scoped map books — one per region — from the same layout, which is the backbone of a scheduled, unattended map-book job. For the broader pattern of running these steps unattended on a server, see batch processing with PyQGIS.
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. The same configured atlas fans out to three different output shapes depending on which overload you call.
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. - Step through pages with
beginRender()/first()/next()/seekTo()to preview extents and labels before a long export. - A data-defined
filenameExpressionsorts per-feature output into subfolders and lets one script emit several scoped map books. - 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 Generate an Atlas PDF in PyQGIS guide shows the full standalone script.
How do I preview a single atlas page without exporting?
Call atlas.beginRender(), then atlas.first() or atlas.seekTo(index) to move the current page; inspect atlas.currentFeature() and atlas.nameForPage(...), and finish with atlas.endRender(). This reframes the controlled map and re-evaluates labels exactly like an export, but writes nothing to disk.
Can I send each page to a different folder?
Yes. Set a data-defined filename with atlas.setFilenameExpression(...) that builds a path from coverage fields — for example prefixing "region" as a subfolder. QGIS evaluates it per feature during exportToPdfs/exportToImage and creates missing subdirectories under an existing parent.