Open a Project with Read Flags in PyQGIS

Opening a project in a script does more work than most scripts need. QGIS validates every layer's data source, reads extents and feature counts, loads every print layout, and pops up a dialog for missing files — fine for a person at the desk, slow or blocking for a script that only wants to list layers, change a path or export one layout. Read flags tell QgsProject.read what to skip, and a custom bad-layer handler replaces the dialog with code, so scripts open projects quickly and never hang waiting for a click.

This recipe belongs to Working with QGIS Projects. It opens projects with flags that skip layouts and data checks, trusts stored layer metadata, handles missing layers in code, inspects project files without loading data, and processes many projects in a batch.

What reading a project normally doesA normal project read parses the project file, opens every layer's data source to check it, reads extents and statistics, loads every print layout with its items, and shows a dialog for layers whose data is missing. Read flags can skip loading layouts, avoid opening data sources by trusting stored metadata, and skip loading layers entirely, and a bad layer handler replaces the dialog.Each step can be skipped or replacedparse filealwaysopen sourcestrust storedmetadataload layoutsDontLoadLayoutsmissing datadialog →your handler

Prerequisites

Read a project with flags

QgsProject.read takes a path and a combination of read flags. The most useful for scripts skip layouts and avoid touching data sources.

import time
from qgis.core import QgsProject, Qgis

project = QgsProject.instance()
flags = (Qgis.ProjectReadFlag.DontLoadLayouts
         | Qgis.ProjectReadFlag.TrustLayerMetadata
         | Qgis.ProjectReadFlag.DontLoad3DViews)

start = time.perf_counter()
ok = project.read("/data/projects/city_atlas.qgz", flags)
print(ok, f"{time.perf_counter() - start:.2f} s,", len(project.mapLayers()), "layers")

Breakdown: DontLoadLayouts skips building print layouts, which in projects with many layouts and atlases can be the slowest part of opening. TrustLayerMetadata uses the extents and other metadata stored in the project instead of querying each data source — a large saving for PostGIS and web-service layers, where each check is a network round trip. DontLoad3DViews skips 3D map view configurations. On QGIS releases before 3.26 the flags live on QgsProject.ReadFlag instead of Qgis.ProjectReadFlag.

Choose flags for the job

The right combination depends on what the script will do with the project.

Flags by taskTo list or edit layer paths and properties: DontResolveLayers keeps layers unloaded and is fastest. To render maps or run analysis without layouts: DontLoadLayouts plus TrustLayerMetadata. To export layouts: load layouts but trust metadata. To inspect styles or variables only: DontResolveLayers and DontLoadLayouts. Using too many flags breaks tasks that need the skipped parts.Skip what the task does not neededit pathsDontResolveLayersfastestanalysisDontLoadLayoutsTrustLayerMetadatalayout exportTrustLayerMetadatalayouts neededinspect onlyDontResolveLayersDontLoadLayouts

DontResolveLayers goes furthest: layers are created from their project definition but their data providers are not opened, so the layer objects exist with names, sources, styles and ids but cannot be drawn or queried. That is ideal for scripts that rewrite data source paths, audit styles or change variables and save. For analysis or rendering, layers must resolve, so use TrustLayerMetadata and skip layouts instead. A script that exports a layout obviously must load layouts.

Handle missing layers in code

When a layer's data cannot be found, QGIS Desktop shows the "Handle unavailable layers" dialog. Scripts need a handler that decides without asking — log the problem, try a known relocation, or leave the layer invalid.

from qgis.core import QgsProjectBadLayerHandler
from pathlib import Path

class RelocatingHandler(QgsProjectBadLayerHandler):
    def __init__(self, old_root, new_root):
        super().__init__()
        self.old_root, self.new_root = old_root, new_root
        self.report = []

    def handleBadLayers(self, layers):
        for node in layers:
            ds_elem = node.firstChildElement("datasource")
            source = ds_elem.text()
            name = node.firstChildElement("layername").text()
            candidate = source.replace(self.old_root, self.new_root)
            path_part = candidate.split("|")[0]
            if candidate != source and Path(path_part).exists():
                ds_elem.firstChild().setNodeValue(candidate)
                self.report.append((name, "relocated"))
            else:
                self.report.append((name, f"missing: {source}"))

handler = RelocatingHandler("//oldserver/gis", "/mnt/gis")
project.setBadLayerHandler(handler)
project.read("/data/projects/city_atlas.qgz", Qgis.ProjectReadFlag.DontLoadLayouts)
for row in handler.report:
    print(*row)

Breakdown: QGIS calls handleBadLayers with the XML nodes of every layer whose source failed to open. The handler can rewrite the datasource text in the node — here replacing an old server root with its new mount point when the file exists there — and QGIS then retries those layers. Layers it cannot fix stay invalid and are recorded. The project instance takes ownership of the handler; keep the Python reference for the report. The broader repair workflow, including saving the fixed project, is in fixing broken layer paths in a project.

Measure the gain on your own projects

How much flags save depends entirely on the project: a file-based project with two layouts barely changes, a project with fifty PostGIS layers and an atlas can open ten times faster. Timing a few combinations on a representative project shows which flags are worth it.

def timed_read(path, flags):
    project.clear()
    t = time.perf_counter()
    project.read(path, flags)
    return time.perf_counter() - t

path = "/data/projects/city_atlas.qgz"
combos = {
    "default": Qgis.ProjectReadFlags(),
    "no layouts": Qgis.ProjectReadFlag.DontLoadLayouts,
    "trust metadata": Qgis.ProjectReadFlag.TrustLayerMetadata,
    "both": Qgis.ProjectReadFlag.DontLoadLayouts | Qgis.ProjectReadFlag.TrustLayerMetadata,
    "unresolved": Qgis.ProjectReadFlag.DontResolveLayers | Qgis.ProjectReadFlag.DontLoadLayouts,
}
for label, flags in combos.items():
    print(f"{label:<16} {timed_read(path, flags):6.2f} s")

Breakdown: Clearing before each read makes the timings comparable, though the first read in a session also pays for initialising providers and should be repeated or discarded. Network-backed layers dominate when TrustLayerMetadata helps; many layouts dominate when skipping layouts helps; both matter for large atlas projects. Record the result alongside the script that uses the flags, so the reason for them is not forgotten.

Inspect a project without loading it

Sometimes even a fast read is too much — a script indexing thousands of projects just needs their titles, layer sources and CRS. A .qgz is a zip containing the .qgs XML, which can be parsed directly.

import zipfile
import xml.etree.ElementTree as ET

def project_summary(path):
    with zipfile.ZipFile(path) as z:
        qgs_name = next(n for n in z.namelist() if n.endswith(".qgs"))
        root = ET.fromstring(z.read(qgs_name))
    title = root.findtext("title") or Path(path).stem
    crs = root.findtext("projectCrs/spatialrefsys/authid")
    layers = [(ml.findtext("layername"), ml.findtext("provider"), ml.findtext("datasource"))
              for ml in root.iter("maplayer")]
    return {"title": title, "crs": crs, "version": root.get("version"), "layers": layers}

s = project_summary("/data/projects/city_atlas.qgz")
print(s["title"], s["crs"], s["version"], len(s["layers"]), "layers")

Breakdown: Reading the XML with the standard library needs no QGIS at all, so it runs anywhere — a web service, a CI job — in milliseconds per project. The QGIS version that saved the project is in the root element's version attribute, useful for finding projects that need updating. This is read-only; for changes, open the project with QGIS and DontResolveLayers so it is written back correctly.

Process many projects in a batch

Combining flags, a handler and a loop processes a whole folder of projects — for example, reporting every layer that points to a decommissioned server.

A project audit loopFor each project file in a folder, the script clears the project instance, reads the file with DontResolveLayers and DontLoadLayouts, collects every layer's source, checks it against rules such as forbidden server paths, and writes one report row per finding. Projects that fail to read are reported too. Nothing is modified.Read light, check, report*.qgzhundredsread lightDontResolveLayersDontLoadLayoutsreportone row per issue

import csv

rows = []
light = Qgis.ProjectReadFlag.DontResolveLayers | Qgis.ProjectReadFlag.DontLoadLayouts
for qgz in sorted(Path("/data/projects").rglob("*.qgz")):
    project.clear()
    if not project.read(str(qgz), light):
        rows.append((qgz.name, "", "", "could not read project"))
        continue
    for layer in project.mapLayers().values():
        src = layer.source()
        if "//oldserver/" in src or src.startswith("C:\\Users"):
            rows.append((qgz.name, layer.name(), layer.providerType(), src))

with open("/data/projects/source_audit.csv", "w", newline="", encoding="utf-8") as fh:
    csv.writer(fh).writerows([("project", "layer", "provider", "source")] + rows)
print(len(rows), "findings")

Breakdown: project.clear() resets the singleton between files so layers from one project never leak into the next. With layers unresolved and layouts skipped, each read takes a fraction of a second even for large projects, because nothing touches the network or disk beyond the project file. The rules — an old server name, personal folders — catch the sources that break when projects are shared. For changes rather than audits, set new sources and call project.write() per file, keeping backups.

QGIS version compatibility

Read flags are available on QGIS 3.34 LTR, 3.40 LTR and QGIS 4 as Qgis.ProjectReadFlag (moved from QgsProject.ReadFlag in 3.26). TrustLayerMetadata, DontResolveLayers, DontLoadLayouts and DontLoad3DViews exist on all of these; newer releases add further flags such as skipping project styles. QgsProjectBadLayerHandler subclassing works on all current versions.

Troubleshooting

  • Layers cannot be drawn after reading. DontResolveLayers was used; read without it for rendering.
  • Extents are wrong. TrustLayerMetadata used stale stored extents; read without it or update extents.
  • The script hangs. A bad-layer dialog is waiting in a GUI session; set a handler.
  • Layers from a previous project remain. project.clear() was not called between reads.

Conclusion

Open projects with flags that match the task — skip layouts for analysis, trust stored metadata to avoid source checks, leave layers unresolved for editing paths and auditing — replace the unavailable-layers dialog with a handler that relocates or records missing data, parse .qgz XML directly for inventories, and clear the project between reads in batches.

Frequently Asked Questions

Does TrustLayerMetadata change saved projects? No; it affects only how this read gathers metadata.

Can I read a project without the singleton instance? Yes — create QgsProject() objects for parallel or isolated reads, but many APIs default to the instance.

Why is the first read slower than later ones? Providers, CRS databases and caches initialise on first use.

Can I save a project read with DontResolveLayers? Yes, and that is the usual purpose: layer definitions, styles and the changed sources are written back correctly even though no data was opened.

What about projects stored in a database? Projects saved in PostgreSQL or GeoPackage are read with a URI such as postgresql:?service=gis&schema=projects&project=atlas; the same flags apply.

Can read flags be used in QGIS Desktop? The "trust project when data sources are unavailable" option in project properties corresponds to trusting metadata.