Generate a Report with QgsReport in PyQGIS
An atlas produces one page per feature from a single layout. That covers a map book, but many deliverables have more structure: a cover and introduction, then a chapter per district with its own title page, then a page per site within each district, then a summary table at the end. QGIS reports model exactly that — a tree of sections, each with an optional header, body and footer layout, where grouped sections iterate over a layer by a field — and the whole report exports to one PDF.
This recipe belongs to Automated Map Layout Generation with PyQGIS. It builds a report with a static cover, a grouped section per region, a nested body per feature, and a summary, then exports it and registers it with the project so it can be edited in the report designer.
Prerequisites
- QGIS 3.40 LTR or newer, or the QGIS 4 series. Reports have existed since 3.2.
- Layout templates (
.qpt) for each page type — cover, section header, feature page, summary — designed in the layout designer. Building every item in code is possible but slow to iterate on; templates keep the design editable. - Layers with a grouping field, for example sites with a
regionattribute. Grouped sections expect the layer to be groupable by that field.
Load page templates as layouts
Each section's header, body and footer is an ordinary QgsLayout. Loading them from templates keeps design in the designer and logic in the script.
from qgis.PyQt.QtXml import QDomDocument
from qgis.core import QgsProject, QgsLayout, QgsReadWriteContext
project = QgsProject.instance()
def layout_from_template(path):
layout = QgsLayout(project)
layout.initializeDefaults()
doc = QDomDocument()
with open(path, encoding="utf-8") as fh:
doc.setContent(fh.read())
items, ok = layout.loadFromTemplate(doc, QgsReadWriteContext(), True)
if not ok:
raise RuntimeError(f"could not load template {path}")
return layout
cover = layout_from_template("/srv/templates/report_cover.qpt")
region_header = layout_from_template("/srv/templates/region_header.qpt")
site_page = layout_from_template("/srv/templates/site_page.qpt")
summary = layout_from_template("/srv/templates/summary.qpt")
Breakdown: loadFromTemplate with True clears the default page first, so the template's page size and items are used as designed. Templates can contain map items, labels with expressions, attribute tables, legends and pictures; for the section pages, the important part is that labels and tables use expressions referring to the current feature, which the report supplies at export time. Laying out the same templates by hand is covered in adding a map item and setting its extent.
Build the section tree
A QgsReport is the root section. Field group sections iterate a layer, grouped and sorted by a field; plain layout sections print once. Sections are added as children in the order they should appear.
from qgis.core import QgsReport, QgsReportSectionFieldGroup, QgsReportSectionLayout
regions = project.mapLayersByName("regions")[0]
sites = project.mapLayersByName("inspection_sites")[0]
report = QgsReport(project)
report.setName("Annual Inspection Report 2026")
report.setHeaderEnabled(True)
report.setHeader(cover)
by_region = QgsReportSectionFieldGroup(report)
by_region.setLayer(regions)
by_region.setField("region_name")
by_region.setSortAscending(True)
by_region.setHeaderEnabled(True)
by_region.setHeader(region_header)
report.appendChild(by_region)
per_site = QgsReportSectionFieldGroup(by_region)
per_site.setLayer(sites)
per_site.setField("site_id")
per_site.setSortAscending(True)
per_site.setBodyEnabled(True)
per_site.setBody(site_page)
by_region.appendChild(per_site)
closing = QgsReportSectionLayout(report)
closing.setBodyEnabled(True)
closing.setBody(summary)
report.appendChild(closing)
Breakdown: Headers, bodies and footers take ownership of the layouts passed to them, so do not reuse one QgsLayout object in two places — load the template twice. A field group section produces one iteration per distinct value of its field; with a header enabled, that header prints at the start of each group. Nesting per_site inside by_region is what filters sites to the current region, provided the child layer relates to the parent — see the next section. The summary is a static section, printed once after all regions. Setting the header on the root report gives a cover page printed exactly once.
Filter child sections to their parent
A nested field group is filtered to the current parent feature when QGIS can relate the two layers. The dependable way to guarantee that is a project relation between the parent and child layers on the shared key.
from qgis.core import QgsRelation
rel = QgsRelation()
rel.setId("regions_sites")
rel.setName("sites in region")
rel.setReferencedLayer(regions.id())
rel.setReferencingLayer(sites.id())
rel.addFieldPair("region_name", "region_name")
if not rel.isValid():
raise RuntimeError(rel.validationError())
project.relationManager().addRelation(rel)
Breakdown: The referencing layer (sites) holds the foreign key; the referenced layer (regions) holds the key it points at. With the relation in place, the child section's iteration is restricted to features related to the parent's current feature, so each region's chapter lists only its own sites. Relations are useful far beyond reports — forms and expressions use them too — and building them is covered in defining layer relations. Check a small export before the full run: the symptom of a missing link is a report that looks correct for the first region and is enormously long.
Export to PDF and register the report
A report is a layout iterator, so the layout exporter writes it to one PDF in a single call. Registering it with the project's layout manager makes it available in the report designer and saves it with the project.
from qgis.core import QgsLayoutExporter
settings = QgsLayoutExporter.PdfExportSettings()
settings.dpi = 200
settings.rasterizeWholeImage = False
settings.forceVectorOutput = True
settings.appendGeoreference = True
result, error = QgsLayoutExporter.exportToPdf(
report, "/data/reports/inspection_report_2026.pdf", settings)
if result != QgsLayoutExporter.Success:
raise RuntimeError(f"export failed ({result}): {error}")
manager = project.layoutManager()
existing = manager.layoutByName(report.name())
if existing:
manager.removeLayout(existing)
manager.addLayout(report)
project.write()
Breakdown: The static exportToPdf overload that takes an iterator walks every section in order and writes one document, with page numbers continuing across sections. forceVectorOutput keeps map items as vectors for sharp printing; set it false and lower the DPI for smaller files when maps contain heavy imagery. The returned error string names the failing page when one does. Removing a report with the same name before adding keeps reruns from creating duplicates, and addLayout transfers ownership of the report to the project, so it survives the script. Export settings are the same ones used for exporting multiple layouts to PDF.
Report or atlas?
Both iterate features into pages, so the choice is about structure. An atlas is one layout repeated over one coverage layer, which makes it simpler, faster to set up and easier to split into one PDF per feature, as in exporting atlas pages to individual PDFs. A report is the right tool when the document has distinct page types, groups with their own title pages, nested levels, or fixed front and back matter. A useful rule: if you catch yourself merging several atlas PDFs with a separate cover page, you wanted a report.
Performance is the other consideration. A report re-renders every map item on every body page, exactly like an atlas, so a 400-site report with two maps per page costs the same render time as an 800-page atlas. Keep heavy layers — imagery, dense point clouds, hillshades — out of section maps where they add little, give each map item a sensible scale range so it does not draw the whole region at site level, and export a filtered test report of two regions while the templates are still changing. The full run can then go to a scheduled job overnight.
QGIS version compatibility
QgsReport, QgsReportSectionFieldGroup and QgsReportSectionLayout have been available since QGIS 3.2 and have kept the same API. QgsLayoutExporter.exportToPdf with an iterator is 3.0+. On the QGIS 4 series, compare the result with QgsLayoutExporter.ExportResult.Success. Layout templates saved from 3.x load in later versions; templates saved in a newer version may not load in older ones.
Troubleshooting
- Every group lists every child feature. No relation or shared field links the nested layers.
- The export has only the cover. Child sections were created but never added with
appendChild. - QGIS crashes after export. A layout was assigned to two sections; each needs its own instance.
- Labels show the same values on every page. Label expressions reference fields without the report supplying a feature; check the section has a layer set.
- The report disappears after closing the project. It was not added to the layout manager before saving.
Conclusion
Design each page type as a template, build the section tree with a QgsReport root, field group sections for each grouping level and layout sections for fixed pages, and link nested layers with a relation so children follow their parent. Export the whole tree to one PDF with QgsLayoutExporter, and register the report with the project so it stays editable.
Frequently Asked Questions
Can a section's body contain an attribute table filtered to the current group? Yes. Set the table's source to the child layer and filter by the current feature's key, the same technique used in adding an attribute table to a layout.
Can I skip a group with no children? Set the header visibility on the field group section to show only when there are features, where your version provides it, or filter the parent layer beforehand.
Can reports be exported to images instead?
Yes. QgsLayoutExporter.exportToImage accepts the report as an iterator and writes one image per page.
Do reports run headless?
Yes. Everything shown uses qgis.core and works in standalone scripts and scheduled jobs.