Generate an Atlas PDF in PyQGIS
This is the concrete, end-to-end recipe: take an existing QGIS project that contains a layout with an atlas-driven map, enable the atlas from code, point it at a coverage layer, optionally filter the features, and export every page to a PDF — either one combined document or one file per feature. The script runs in the QGIS Python Console and, with a small bootstrap, headlessly on a server. It is the applied counterpart to Automating Atlas Map Series in PyQGIS, which explains the atlas object model this recipe drives.
If your maps live in separate layouts rather than a single atlas, the non-atlas pattern in exporting multiple QGIS layouts to PDF is the better fit. Use this page when one layout fans out into many pages from a coverage layer.
Prerequisites
- QGIS 3.34 LTR (Python 3.12) recommended; works on 3.28 LTR and the 3.40/3.44 line.
- A saved project (
.qgzor.qgs) containing a print layout with a map item set to "controlled by atlas". - A coverage layer loaded in that project (the layer whose features become pages).
- Write access to an output directory.
The layout's atlas can already be configured in the project, or you can configure it entirely from code as shown below — the script does not assume the project file has the atlas pre-enabled.
Step 1: Load the Project and Find the Layout
Read the project, then fetch the layout by name through the layout manager. Fail fast if either is missing.
from qgis.core import QgsProject
def open_layout(project_path: str, layout_name: str):
project = QgsProject.instance()
if not project.read(project_path):
raise RuntimeError(f"Could not read project: {project_path}")
layout = project.layoutManager().layoutByName(layout_name)
if layout is None:
available = [l.name() for l in project.layoutManager().layouts()]
raise ValueError(
f"Layout '{layout_name}' not found. Available: {available}"
)
return project, layout
Breakdown: project.read(project_path) loads the .qgz/.qgs into the singleton QgsProject.instance() and returns False on failure. layoutManager().layoutByName(...) returns the named print layout or None; listing available names in the error makes a typo obvious immediately. Returning both project and layout lets later steps look up the coverage layer through the same project.
Step 2: Enable the Atlas and Set Coverage + Filter
Grab the layout's QgsLayoutAtlas, enable it, bind the coverage layer, and apply an optional filter. Recompute the page list before checking the count.
def configure_atlas(project, layout, coverage_name, filter_expr=None):
atlas = layout.atlas()
atlas.setEnabled(True)
coverage_layers = project.mapLayersByName(coverage_name)
if not coverage_layers:
raise ValueError(f"Coverage layer '{coverage_name}' not in project")
atlas.setCoverageLayer(coverage_layers[0])
if filter_expr:
atlas.setFilterFeatures(True)
atlas.setFilterExpression(filter_expr)
# Name pages so per-feature files sort and read cleanly
atlas.setPageNameExpression(
"lpad(@atlas_featurenumber, 3, '0')"
)
atlas.updateFeatures()
page_count = atlas.count()
if page_count == 0:
raise RuntimeError(
"Atlas has 0 pages. Check the coverage layer and filter expression."
)
print(f"Atlas ready: {page_count} pages")
return atlas
Breakdown: layout.atlas() returns the layout's single atlas; setEnabled(True) activates it. setCoverageLayer(...) binds the loaded layer whose features become pages. An optional filter_expr (any valid QGIS expression over coverage fields, e.g. "region" = 'North') trims the series. updateFeatures() is essential — it rebuilds the filtered page list so count() is accurate, and a zero-page result is caught before wasting time on an empty export.
Step 3: Export Every Page to PDF
Build a QgsLayoutExporter and call the atlas overload. Choose a single combined PDF or one PDF per feature.
import os
from qgis.core import QgsLayoutExporter
def export_atlas_pdf(layout, atlas, out_dir, base_name, per_feature=False):
os.makedirs(out_dir, exist_ok=True)
exporter = QgsLayoutExporter(layout)
settings = QgsLayoutExporter.PdfExportSettings()
settings.dpi = 300
settings.forceVectorOutput = True
settings.rasterizeWholeImage = False
if per_feature:
# One PDF per feature; filenames from pageNameExpression
result, error = exporter.exportToPdfs(
atlas, os.path.join(out_dir, base_name), settings
)
else:
# One combined multi-page PDF
result, error = exporter.exportToPdf(
atlas, os.path.join(out_dir, f"{base_name}.pdf"), settings
)
if result != QgsLayoutExporter.Success:
raise RuntimeError(f"Atlas PDF export failed: {error}")
print(f"Export complete -> {out_dir}")
Breakdown: Passing atlas as the first argument is what makes the exporter iterate the series rather than render one page. exportToPdf(atlas, path, settings) produces one combined document; exportToPdfs(atlas, base_path, settings) writes a separate file per feature, named from the pageNameExpression set in step two. The atlas overloads return a (result, error) tuple — note this differs from the single-layout exportToPdf, which returns only a status code. forceVectorOutput = True keeps text and lines crisp; rasterizeWholeImage = False avoids flattening the whole page to a bitmap.
Step 4: Run It All Together
def main():
project, layout = open_layout(
"/data/projects/districts.qgz",
layout_name="DistrictMaps",
)
atlas = configure_atlas(
project, layout,
coverage_name="districts",
filter_expr='"status" = \'active\'',
)
export_atlas_pdf(
layout, atlas,
out_dir="/data/atlas_out",
base_name="district_atlas",
per_feature=False, # True for one PDF per district
)
# In the QGIS Python Console:
# main()
Breakdown: The three functions compose into a linear flow: open, configure, export. Switching per_feature between False and True is the only change needed to toggle between a single book and individual sheets. Because each function raises on its own failure mode, a problem (missing project, missing coverage layer, empty filter, export error) is reported at the exact stage it occurs.
Running Headless (Standalone Script)
For cron jobs, containers, or CI, bootstrap QGIS before any of the above runs. No GUI or iface is required for atlas export.
from qgis.core import QgsApplication
qgs = QgsApplication([], False)
qgs.setPrefixPath("/usr", True) # adjust to your install prefix
qgs.initQgis()
# main() # call the workflow defined above
qgs.exitQgis()
Breakdown: QgsApplication([], False) starts QGIS with no GUI; setPrefixPath points it at the install so providers and data paths resolve. initQgis() must run before reading projects or exporting. Atlas export depends only on the layout and project, not on a map canvas, so it works fully headless. Always pair initQgis() with exitQgis() to flush and release resources — important for long-running batch servers. This is the same headless pattern used across Spatial Data Processing & Automation with PyQGIS pipelines.
Optional: Per-Feature Filenames and Tuning DPI
When you export one PDF per feature, the filenames come straight from the atlas pageNameExpression. Make it produce a readable, sortable, filesystem-safe string and you get a clean folder of named sheets with no extra code.
# A descriptive, sortable, safe page name -> "012_riverside_north"
atlas.setPageNameExpression(
"lpad(@atlas_featurenumber, 3, '0') || '_' || "
"lower(replace(\"district_name\", ' ', '_'))"
)
Breakdown: lpad(@atlas_featurenumber, 3, '0') zero-pads the page index so 012 sorts before 100; concatenating the lower-cased, space-stripped district name yields a filename that is both human-readable and safe across operating systems. Because exportToPdfs derives each filename from this expression, controlling the expression is the only thing needed to control output naming.
For tuning quality versus size: settings.dpi drives raster resolution (300 for print, 150 for quick previews), forceVectorOutput = True keeps vector layers sharp regardless of DPI, and for very large series consider exporting per-feature to cap individual file size and memory pressure rather than one giant document.
QGIS Version Compatibility
Baseline: QGIS 3.34 LTR (Python 3.12). The atlas export API is stable across the current LTR and latest lines.
| QGIS / Python | Notes |
|---|---|
| 3.28 LTR / Py 3.9 | Atlas overloads of QgsLayoutExporter available and return (result, error). exportToPdfs present. |
| 3.34 LTR / Py 3.12 (baseline) | Recommended. Behaviour as documented here. |
| 3.40 / 3.44 / Py 3.12 | Same API. Some PdfExportSettings fields added; existing fields unchanged. |
The single biggest cross-version gotcha is the return signature: the atlas exportToPdf(atlas, ...) returns a tuple, while the single-layout exportToPdf(path, ...) returns one status code. Unpack accordingly or you will hit a cannot unpack / comparison error.
Troubleshooting
Atlas has 0 pages. The filter excluded everything, the coverage layer is empty, or you set the filter without calling updateFeatures(). Verify the expression against the coverage layer's fields and confirm atlas.count() after updateFeatures().
cannot unpack non-iterable or a comparison failure on the export result. You used the single-layout call signature. With an atlas, exportToPdf(atlas, ...) returns (result, error) — unpack two values.
The PDF renders but the map frame does not move per page. The map item is not atlas-controlled. In the layout, enable "Controlled by atlas" on the map item (or set it in code), otherwise every page shows the same extent.
Blank pages or missing layers. A data source path in the project is broken, or layers are toggled off. Re-open the project in QGIS, fix broken sources, and confirm the relevant layers are visible before scripting the export.
Export fails with a file error on Windows. The target PDF is open in a viewer, or antivirus/cloud-sync locks the folder. Export to a local temp directory, then move the files.
Conclusion
Generating an atlas PDF from PyQGIS is a tight three-step flow: load the project and layout, enable and configure the atlas (coverage, filter, page names, updateFeatures), then export with the atlas-aware QgsLayoutExporter overloads. Toggle one flag to switch between a combined book and per-feature sheets, and add a QgsApplication bootstrap to run the whole thing unattended. With the export driven by code, your map book regenerates automatically whenever the coverage data changes — no dialogs, no manual paging.
Frequently Asked Questions
How do I produce one PDF per feature instead of a single document?
Call exporter.exportToPdfs(atlas, base_path, settings) instead of exportToPdf. Filenames come from the atlas pageNameExpression, so set that to something unique per feature.
Do I need the QGIS GUI or iface to export an atlas?
No. Atlas export depends only on the project and layout. Bootstrap with QgsApplication([], False) and initQgis(), and it runs fully headless.
Why is the export result not a simple success code?
The atlas overloads of exportToPdf/exportToPdfs/exportToImage return a (result, error) tuple. Unpack both, then compare result == QgsLayoutExporter.Success.
Can I filter which features become pages?
Yes. Call atlas.setFilterFeatures(True) and atlas.setFilterExpression(expr) with any valid QGIS expression over the coverage layer's fields, then atlas.updateFeatures() and check atlas.count().