Apply a Map Theme to a Layout Map in PyQGIS

A layout map item shows whatever the canvas shows, which is convenient until you need four maps that differ only in content. Point the item at a map theme instead and the template stops depending on what happens to be ticked: the item renders the theme, the legend follows it, and a loop over the theme names produces the whole set.

This recipe belongs to Map Themes & Layer Visibility in PyQGIS. It covers linking an item to a theme, the three ways a layout map decides what to draw, keeping a legend in step, driving several map items from different themes on one page, and exporting a series.

Three things a layout map item can followBy default a layout map item mirrors the canvas, so its content changes whenever someone ticks a layer. Setting a fixed layer list pins it to specific layers but not to their styles. Following a map theme pins both the layers and the styles they use, and lets a legend follow the same theme.Only one of these survives somebody ticking a layerfollows the canvasthe defaultwhatever is ticked nowchanges under youbetween exportsfixed layer listsetLayers(…)layers pinnedbut styles still changeif somebody restyles a layerfollows a themesetFollowVisibilityPreset(True)layers AND styles pinnedthe legend can follow tooone template · n themes · n deliverablesthe loop is four lines

Prerequisites

  • QGIS 3.34 LTR (bundled Python 3.12) or newer.
  • A project containing at least two map themes.
  • A layout with a map item whose id has been set in its item properties; positional lookup works but breaks the first time somebody rearranges the page.

Two calls, and the order does not matter as long as both are made.

from qgis.core import QgsProject

project = QgsProject.instance()
layout = project.layoutManager().layoutByName("A3 landscape")
map_item = layout.itemById("main map")

map_item.setFollowVisibilityPreset(True)
map_item.setFollowVisibilityPresetName("flood risk")
layout.refresh()

Breakdown: The API says preset where the interface says theme; they are the same thing, and the old name survives for compatibility. Setting only the name does nothing at all — the boolean is the switch, and forgetting it is the single most common failure here, made worse by the fact that the layout keeps rendering the canvas quite happily so nothing looks broken. layout.refresh() invalidates the item's cached render; without it an export can write the previous theme under the new name.

itemById() returns None for an id that does not exist, so a script that will run unattended should say so plainly:

map_item = layout.itemById("main map")
if map_item is None:
    raise LookupError("layout has no item with id 'main map'")

Breakdown: The alternative — iterating layout.items() and matching on type — finds a map item rather than the map item, which is fine on a single-map page and wrong on a page with an inset. Setting ids in the layout is a two-second job that removes this whole class of ambiguity.

Keep the legend honest

A legend that lists layers the map is not showing is worse than no legend. QgsLayoutItemLegend can follow the same theme.

legend = layout.itemById("main legend")
legend.setLinkedMap(map_item)
legend.setLegendFilterByMapEnabled(True)
legend.setAutoUpdateModel(True)
layout.refresh()

Breakdown: setLinkedMap() tells the legend which map item it describes, which is what allows the other two settings to mean anything. setLegendFilterByMapEnabled(True) removes entries for layers with no features inside the map item's extent — a genuine improvement on an atlas where most pages contain only some of the classes. setAutoUpdateModel(True) keeps the legend's tree in step with the map's layers; turn it off only when you have hand-edited legend entries you want preserved, because rebuilding the model discards those edits.

A legend linked to a themed mapThe page holds a map item that follows the flood risk theme and a legend linked to that item. Because the legend is linked and filtered by the map, it lists only the three layers the theme shows, and drops entries whose features fall outside the visible extent.The legend describes the map item, not the projectA3 landscapemap item · id "main map"follows theme "flood risk"legendflood extentbuildingsbasemapland use is absentsetLinkedMap(map_item)

Several maps, several themes, one page

A comparison page is the case that makes themes indispensable: the same extent, drawn four ways.

PANELS = {
    "panel nw": "base",
    "panel ne": "flood risk",
    "panel sw": "land use",
    "panel se": "night",
}

reference = layout.itemById("panel nw")
for item_id, theme_name in PANELS.items():
    item = layout.itemById(item_id)
    item.setFollowVisibilityPreset(True)
    item.setFollowVisibilityPresetName(theme_name)
    item.zoomToExtent(reference.extent())
layout.refresh()

Breakdown: Each map item keeps its own theme, its own extent and its own scale, so the four panels are genuinely independent objects that happen to share a page. zoomToExtent() copies the reference panel's extent to the others, which is what makes the comparison fair — four panels at four slightly different extents is the commonest defect in a small-multiples page and the hardest to notice. Note that the items must be the same aspect ratio for identical extents to produce identical framing; a panel of different proportions will pad the extent to fit.

Per-item overrides, for the exceptions

Occasionally one item on the page must break the rule. An inset locator wants only the boundary and a highlight; a detail panel wants a layer that the layout's scale would normally hide. Layout map items carry their own overrides for exactly these cases.

inset = layout.itemById("locator")
inset.setFollowVisibilityPreset(False)
inset.setLayers([
    project.mapLayersByName("boundaries")[0],
    project.mapLayersByName("study area")[0],
])

Breakdown: Turning the theme link off before setting a layer list matters — with the link on, the theme wins and the list is stored but unused, which produces an inset that stubbornly shows the main map's content. setLayers() takes layer objects in draw order, top of the list drawn last, and it is a complete list rather than an addition, so anything omitted is absent from that item regardless of the project state.

The scale-range override is a separate switch on the item, and it is worth knowing about because it explains a whole class of "the layer is missing from one panel" reports:

detail = layout.itemById("panel se")
detail.setFollowVisibilityPreset(True)
detail.setFollowVisibilityPresetName("land use")
detail.setScale(5000)

Breakdown: Setting the scale explicitly recomputes the item's extent about its current centre, which is usually what a fixed-scale detail panel wants. Because scale-based visibility is evaluated against the item's scale rather than the canvas scale, a panel set to 1:5 000 shows layers that are hidden in the main 1:120 000 map — correct behaviour that reliably surprises people the first time.

Making the whole thing reproducible

The value of themes in layouts is that a deliverable becomes a function of the project rather than of what somebody had ticked at the time. That only holds if the wiring itself is scripted.

Keep one build script that creates the themes, sets the item links, and exports — in that order, in one file, run from a clean project open. Anything clicked into place between runs is a difference nobody will remember six months later, and a layout whose map item quietly reverted to following the canvas produces four identical PDFs with four different names. Checking the state before exporting costs three lines and catches exactly that:

assert map_item.followVisibilityPreset(), "map item is not following a theme"
assert map_item.followVisibilityPresetName() in project.mapThemeCollection().mapThemes()

Breakdown: The second assertion catches a theme that was renamed or deleted after the layout was wired up, which leaves the item following a name that no longer resolves — QGIS falls back to the canvas silently rather than complaining. Both checks are cheap enough to leave in permanently.

Export the series

With the item linked, producing the set is a loop.

from qgis.core import QgsLayoutExporter

settings = QgsLayoutExporter.PdfExportSettings()
settings.dpi = 300

for theme_name in project.mapThemeCollection().mapThemes():
    map_item.setFollowVisibilityPresetName(theme_name)
    layout.refresh()
    exporter = QgsLayoutExporter(layout)
    safe = theme_name.replace(" ", "_").replace("/", "-")
    result = exporter.exportToPdf(f"/data/output/{safe}.pdf", settings)
    if result != QgsLayoutExporter.Success:
        raise RuntimeError(f"export failed for {theme_name} with code {result}")

Breakdown: A fresh QgsLayoutExporter per iteration avoids any state carried between exports, and it is cheap. Checking the return code matters because exportToPdf() does not raise — a full disk or an unwritable directory returns a status and the loop otherwise continues happily producing nothing. Sanitising the filename centrally is a small thing that prevents a theme named with a slash from writing into a directory that does not exist. See exporting multiple layouts for the variations on the export settings.

QGIS version compatibility

The examples target QGIS 3.34 LTR (Python 3.12).

QGIS versionPythonNotes
3.16 LTR3.7setFollowVisibilityPreset and the legend link settings all present.
3.22 LTR3.9QgsLayoutExporter.PdfExportSettings gains simplifyGeometries.
3.28 LTR3.9Layout map items can override layer scale ranges per item.
3.34 LTR3.12Baseline for this page.
3.40+3.12Theme-linked legend patch shapes follow the theme's styles.

Troubleshooting

  • The map still shows the canvas. setFollowVisibilityPreset(True) was not called; the name alone does nothing.
  • The export shows the previous theme. layout.refresh() was skipped between setting the name and exporting.
  • itemById() returns None. The item has no id set in its properties. Set one rather than iterating by type.
  • The legend lists layers the map does not show. The legend is not linked to the map item, or setAutoUpdateModel(False) is preserving a stale hand-edited model.
  • Panels are framed slightly differently. The items have different aspect ratios, so a shared extent is padded differently in each. Match the item sizes.
  • The export "succeeded" but wrote nothing. The return code was not checked. Compare against QgsLayoutExporter.Success.

Conclusion

Set both setFollowVisibilityPreset(True) and the theme name, refresh the layout before every export, and link the legend to the map item so it describes what is actually drawn. From there, one template plus the project's theme list generates a whole series, and the only per-deliverable code is a filename.

Frequently Asked Questions

Can an atlas and a theme be used together? Yes, and they are orthogonal — the atlas drives the extent from a coverage feature while the theme drives the content. Looping over themes with an atlas inside produces the full cross product, which is the standard way to generate a set of thematic map books.

Does the theme override the item's layer list? Yes. Following a theme takes precedence over setLayers(), so setting both is confusing rather than additive. Pick one mechanism per item.

Can different pages of one layout follow different themes? A layout is a single page design; multiple pages share the same items unless you add separate map items. For genuinely different content per page, use an atlas or several layouts in the layout manager.

Will the theme's styles show in an exported legend? Yes, when the legend is linked to the map item and auto-updating, the patches are drawn with the styles the theme pinned rather than the layers' current styles.