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.
Prerequisites
- QGIS 3.34 LTR or newer, or the QGIS 4 series.
- A standalone script with an initialised
QgsApplication, or the Python console, as described in running Python scripts outside QGIS Desktop.
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.
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.
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.
DontResolveLayerswas used; read without it for rendering. - Extents are wrong.
TrustLayerMetadataused 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.