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.

A report is a tree of sectionsThe report root has a header layout used as a cover page. Its first child is a field group section over the regions layer grouped by region name, with a header layout printed once per region. That section contains a nested field group section over the sites layer grouped by site id, whose body layout prints one page per site. The report's second child is a static layout section with a summary body. The output PDF reads cover, region 1 header, its site pages, region 2 header, its site pages, and the summary.Sections nest; the PDF reads the tree top to bottomQgsReport · header = coverfield group: regions by nameheader = region title pagefield group: sites by idbody = one page per sitelayout section: summaryreport.pdfcoverNorth regionsite N-01 · N-02 · N-03South regionsite S-01 · S-02summary

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 region attribute. 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.

Children follow their parent groupWhile the parent section is on region North, the child section over sites is restricted to sites whose region matches North. The link comes from a project relation between the regions and sites layers, or from the child layer having the same grouping field as the parent. Without that link, every region would list every site.Without a link, every region lists every sitecurrent regionNorthrelation or fieldregion_name = 'North'site pagesN-01, N-02, N-03no link definedNorth pages list N-01 … S-02 — all sites, every region

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.

Export, and keep it editableThe report object feeds two paths. QgsLayoutExporter.exportToPdf writes the entire report to one PDF with PDF export settings such as DPI and vector export. layoutManager.addLayout registers the report in the project, so it appears in the Layout Manager, can be opened in the report designer and re-exported by others without running the script.One object, two usesQgsReportQgsLayoutExporter.exportToPdfone PDF, all sectionslayoutManager().addLayouteditable in the report designer

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.