Debug Invalid Layer Loading in PyQGIS
QgsVectorLayer("/data/roads.shp", "roads", "ogr") never raises. When something is wrong — a typo in the path, a missing driver, a password that expired, a URI with the wrong quoting — it quietly returns a layer object whose isValid() is False, with no features, no fields and no extent. Scripts that skip the check fail three steps later with a confusing error about an empty extent or a None provider, and the original reason is gone.
This recipe belongs to Debugging PyQGIS Scripts. It reads the errors QGIS does record, works through the checks in the order that finds problems fastest, asks GDAL directly when QGIS is vague, covers databases and web services, and ends with a loading helper that turns every invalid layer into a clear exception.
Prerequisites
- QGIS 3.40 LTR or newer, or the QGIS 4 series.
- A layer that loads as invalid, and the exact source string and provider key that produced it.
- For standalone scripts, a correctly initialised
QgsApplication. An uninitialised application makes every layer invalid, which is a different problem covered in running Python scripts outside QGIS Desktop.
Read the errors QGIS recorded
The layer and its provider both keep an error object. They are often empty, but when they are not, they name the cause directly and save the rest of the investigation.
from qgis.core import QgsVectorLayer, QgsRasterLayer, QgsApplication
source = "/data/work/survey_2026.gpkg|layername=trees"
layer = QgsVectorLayer(source, "trees", "ogr")
print("valid:", layer.isValid())
if not layer.isValid():
print("layer error: ", layer.error().summary() or "(empty)")
provider = layer.dataProvider()
if provider is None:
print("no provider was created — check the provider key")
else:
print("provider error:", provider.error().message() or "(empty)")
print("provider valid:", provider.isValid())
print("prefix path: ", QgsApplication.prefixPath())
Breakdown: error().summary() gives a one-line reason when the layer recorded one; message() on the provider error sometimes adds the underlying library's text. A None data provider means QGIS could not create a provider at all — nearly always a misspelt or unavailable provider key — whereas a provider that exists but is invalid means the provider tried and failed to open the source. Printing the prefix path catches the standalone-script case where QGIS cannot find its own plugins and providers.
Check the provider key and the path
Most invalid layers come from the two arguments people type by hand. Both can be checked in a few lines.
import os
from pathlib import Path
from qgis.core import QgsProviderRegistry
registry = QgsProviderRegistry.instance()
key = "ogr"
print("provider available:", key in registry.providerList())
parts = registry.decodeUri(key, source)
path = parts.get("path")
print("decoded:", parts)
if path:
p = Path(path)
print("absolute:", p.is_absolute(), "| exists:", p.exists(),
"| readable:", os.access(p, os.R_OK), "| size:", p.stat().st_size if p.exists() else None)
print("cwd:", Path.cwd())
if path and path.lower().endswith((".gpkg", ".gdb", ".sqlite")):
names = [d.name() for d in registry.querySublayers(path)]
print("tables in container:", names)
if parts.get("layerName") and parts["layerName"] not in names:
print(f"no table called {parts['layerName']!r} — names are case-sensitive")
Breakdown: providerList() is the authoritative list of provider keys in this installation; a minimal Docker image may lack providers such as wms or mssql that a desktop install has. Decoding the source shows how QGIS actually parsed it, which exposes a stray | or a mangled Windows path immediately. A relative path resolves against the current working directory, which is the script's folder in one context and the home directory in another, so relative paths are the classic cause of "works interactively, fails when scheduled". A zero-byte file or a file that is not readable points at a copy that did not finish or a permissions problem. For containers, listing the real table names — as in listing and loading GeoPackage sublayers — settles layer-name mistakes, including case.
Ask GDAL directly
When QGIS's own errors are empty and the path is fine, open the same source with GDAL or OGR. The driver reports its own error text, which is usually specific: an unsupported format version, a corrupt header, a missing projection file, a locked SQLite database.
from osgeo import gdal, ogr
gdal.UseExceptions()
ogr.UseExceptions()
def probe(path, raster=False):
try:
if raster:
ds = gdal.Open(path)
print("GDAL driver:", ds.GetDriver().ShortName, ds.RasterXSize, "x", ds.RasterYSize)
else:
ds = ogr.Open(path)
print("OGR driver:", ds.GetDriver().GetName(),
"| layers:", [ds.GetLayer(i).GetName() for i in range(ds.GetLayerCount())])
except RuntimeError as exc:
print("driver says:", exc)
probe("/data/work/survey_2026.gpkg")
probe("/data/imagery/ortho_2025.jp2", raster=True)
print("JP2 drivers:", [d for d in ("JP2OpenJPEG", "JP2ECW", "JP2KAK")
if gdal.GetDriverByName(d) is not None])
Breakdown: UseExceptions makes GDAL raise instead of returning None, so the driver's message arrives as the exception text. GDAL is the library underneath the ogr and gdal providers, so if it cannot open the file, QGIS cannot either, and its message is the real reason. Checking which drivers exist is how you discover that a format depends on an optional driver — JPEG 2000, MrSID, some File Geodatabase features — that your installation was built without. For locked databases, the message names the lock; close the other program or copy the file.
Databases and web services
Remote layers can fail at the network, at TLS or a proxy, at authentication, or at permissions on one table. Testing each separately finds the level quickly.
import socket
from qgis.core import QgsDataSourceUri, QgsApplication, QgsProviderRegistry
pg = QgsDataSourceUri("service='gis_prod' sslmode=require authcfg=pg00001 "
"key='fid' table=\"cadastre\".\"parcels\" (geom)")
host = pg.host() or "db.internal"
port = int(pg.port() or 5432)
try:
socket.create_connection((host, port), timeout=5).close()
print("network: ok")
except OSError as exc:
print("network:", exc)
manager = QgsApplication.authManager()
cfg = pg.authConfigId()
print("auth config present:", cfg in manager.configIds() if cfg else "none used")
md = QgsProviderRegistry.instance().providerMetadata("postgres")
try:
conn = md.createConnection(pg.uri(False), {})
tables = [t.tableName() for t in conn.tables("cadastre")]
print("can list schema; parcels visible:", "parcels" in tables)
except Exception as exc:
print("connection:", exc)
Breakdown: A raw socket connection separates "the server is unreachable" from everything above it; when the URI uses a service= entry, the host comes from pg_service.conf and must be looked up there instead. Checking that the authentication configuration exists catches projects copied to a machine where it was never created — the layer then tries to connect with no credentials. The connections API opens a real database session with the same URI and lists tables in the schema, so a table missing from the list is a permissions or naming problem rather than a connection problem. The same idea applies to web services: fetch the capabilities or landing page first, as in loading a WFS layer, and check the network logger in the developer tools panel.
Fail loudly with a loading helper
Once the checks are understood, fold them into one function that every script uses to load layers. An invalid layer becomes an exception with the source, the provider and every reason QGIS and GDAL gave.
from qgis.core import QgsMapLayer
class LayerLoadError(RuntimeError):
pass
def load_layer(source, name, provider="ogr", kind="vector"):
cls = QgsVectorLayer if kind == "vector" else QgsRasterLayer
layer = cls(source, name, provider)
if layer.isValid():
return layer
reasons = []
if provider not in QgsProviderRegistry.instance().providerList():
reasons.append(f"provider {provider!r} not available")
if layer.error().summary():
reasons.append(layer.error().summary())
dp = layer.dataProvider()
if dp is not None and dp.error().message():
reasons.append(dp.error().message())
path = QgsProviderRegistry.instance().decodeUri(provider, source).get("path")
if path and not Path(path).exists():
reasons.append(f"path does not exist: {path}")
elif path:
try:
(gdal.Open if kind == "raster" else ogr.Open)(path)
except RuntimeError as exc:
reasons.append(f"GDAL: {exc}")
raise LayerLoadError(f"could not load {name!r} from {source!r} ({provider}): "
+ ("; ".join(reasons) or "no reason reported"))
roads = load_layer("/data/work/base.gpkg|layername=roads", "roads")
Breakdown: The helper returns immediately in the normal case, so the diagnosis costs nothing when layers load. On failure it collects every source of explanation and raises once, with a message that is useful in a log file read the next morning — the discipline covered in handling errors and logging in unattended scripts. Raising a specific exception class lets callers catch layer failures separately from programming errors. Using the helper consistently removes a whole category of "why is the extent empty" debugging from every later script.
QGIS version compatibility
isValid, error() and QgsProviderRegistry.decodeUri behave the same across QGIS 3.x and the QGIS 4 series. querySublayers needs 3.22 and the database connections API (providerMetadata(...).createConnection) 3.10, with table listing improved through later releases. GDAL's exception mode is recommended explicitly from GDAL 3.7 onwards, which every supported QGIS installer bundles.
Troubleshooting
- Every layer is invalid, even known-good files.
QgsApplicationwas not initialised or its prefix path is wrong. - Valid in the console, invalid in a scheduled job. Relative paths or missing environment variables in the job's context.
- Invalid only on one machine. A missing driver, auth configuration or
pg_service.confentry on that machine. - Raster loads but is blank. Not an invalid layer — check the renderer and statistics instead.
- Intermittently invalid on a network share. A file still being written or locked; copy locally before loading.
Conclusion
Always check isValid(). When it is false, read the layer and provider errors, confirm the provider key and the decoded path, ask GDAL for the driver's own message, and test remote connections level by level. Then put those checks in a loading helper so every future invalid layer arrives as an exception that already explains itself.
Frequently Asked Questions
Why does QGIS not raise an exception for an invalid layer? Because the C++ API reports validity through a flag, and the Python bindings preserve it. Projects routinely contain temporarily unavailable layers, which must load as placeholders rather than abort.
Can a layer become invalid after loading?
Yes — a deleted file or dropped connection. Check isValid() again before long operations on long-lived layers.
How do I see what QGIS Desktop does differently?
Load the layer in Desktop, then compare layer.source() and layer.providerType() with the values in your script; the difference is usually visible at once.
Is setDataSource a fix for an invalid layer?
It is, once you know the correct source — see fixing broken layer paths.