PyQGIS Layer Fails to Load (isValid() Returns False): How to Fix It

Problem statement

The path is right, the file opens in QGIS, and PyQGIS says no:

layer = QgsVectorLayer("data/raw/parcels.gpkg", "parcels", "ogr")
print(layer.isValid())      # False
print(layer.featureCount()) # -1

QgsVectorLayer never raises. It returns an object whose isValid() is False, and everything downstream then behaves strangely: a feature count of -1, an empty iteration, a Processing algorithm that fails with "Could not load source layer for INPUT". The failure is silent by design, which is why an explicit validity check is mandatory in every PyQGIS script.

Common causes:

  • the path is relative and the process working directory is not what you assume
  • a multi-layer source (GeoPackage, GeoDatabase, KML) needs |layername= and got none
  • the wrong provider key: "ogr" for vectors, "gdal" for rasters, "postgres", "delimitedtext", "memory"
  • QGIS was not initialised, so no providers are registered yet
  • a missing shapefile sidecar (.shx, .dbf) or a locked file
  • the layer name in a container does not match exactly, including case
  • a database URI with a wrong port, table, geometry column or credentials

Quick answer

To diagnose a layer that will not load:

  1. resolve the path to absolute and confirm it exists on disk
  2. check layer.isValid() immediately and read layer.dataProvider().error().message()
  3. list the layers in a container before naming one
  4. pass the right provider key and the right URI form for the source
  5. make a helper that raises with the URI and the provider message, and use it everywhere
from pathlib import Path
from qgis.core import QgsVectorLayer

def load_vector(uri: str, name: str = "layer") -> QgsVectorLayer:
    layer = QgsVectorLayer(uri, name, "ogr")
    if not layer.isValid():
        provider_msg = layer.dataProvider().error().message() if layer.dataProvider() else ""
        raise RuntimeError(f"could not load {uri!r} β€” {provider_msg or 'no provider message'}")
    return layer

path = Path("data/raw/parcels.gpkg").resolve()
layer = load_vector(f"{path}|layername=parcels", "parcels")
print(layer.featureCount(), "features,", layer.crs().authid())

The provider's error message is the piece almost everyone misses. It usually names the real problem β€” a missing file, an unknown layer, a refused connection β€” where isValid() only says "no".

The URI is where the answer lives

Anatomy of PyQGIS data source URIs for GeoPackage, shapefile, PostGIS and delimited text.
Each provider has its own URI grammar β€” half of all load failures are a malformed one.

Step-by-step solution

Checklist of validity checks to run when a PyQGIS layer will not load.
Six checks, in order β€” each rules out one whole class of cause.

Check the file before blaming the API

from pathlib import Path

p = Path("data/raw/parcels.gpkg")
print("resolved :", p.resolve())
print("exists   :", p.exists())
print("size     :", p.stat().st_size if p.exists() else "-")
print("cwd      :", Path.cwd())

A relative path is resolved against the process working directory, which under a scheduler is rarely the project folder. Always hand PyQGIS an absolute path β€” str(Path(...).resolve()) β€” and the entire category disappears.

Read the provider's error message

layer = QgsVectorLayer(uri, "parcels", "ogr")
if not layer.isValid():
    provider = layer.dataProvider()
    print("valid    :", layer.isValid())
    print("error    :", layer.error().message())
    print("provider :", provider.error().message() if provider else "no provider created")
    print("source   :", layer.source())

"no provider created" means the provider key itself is unknown β€” usually because QGIS was not initialised, or you passed "gdal" for a vector.

Turn on the message log to see what the provider logged internally:

from qgis.core import QgsApplication

QgsApplication.messageLog().messageReceived.connect(
    lambda msg, tag, level: print(f"[{tag}] {msg}")
)

List the layers in a container first

A GeoPackage can hold many layers, and naming the file alone loads the first one β€” or nothing.

from osgeo import ogr

ds = ogr.Open("data/raw/atlas.gpkg")
print([ds.GetLayerByIndex(i).GetName() for i in range(ds.GetLayerCount())])

Or without GDAL's Python bindings:

from qgis.core import QgsProviderRegistry

parts = QgsProviderRegistry.instance().querySublayers("data/raw/atlas.gpkg")
for sub in parts:
    print(sub.name(), "|", sub.uri(), "|", sub.wkbType())

Then build the URI with the exact name, which is case-sensitive:

uri = f"{Path('data/raw/atlas.gpkg').resolve()}|layername=parcels_2026"

Use the right provider key and URI form

from qgis.core import QgsVectorLayer, QgsRasterLayer

# vector file (GeoPackage, shapefile, GeoJSON, FlatGeobuf …)
QgsVectorLayer("/abs/parcels.gpkg|layername=parcels", "parcels", "ogr")

# raster file
QgsRasterLayer("/abs/dem.tif", "dem", "gdal")

# CSV with coordinates
QgsVectorLayer(
    "file:///abs/points.csv?delimiter=,&xField=lon&yField=lat&crs=EPSG:4326",
    "points", "delimitedtext",
)

# in-memory scratch layer
QgsVectorLayer("Point?crs=EPSG:27700&field=id:integer", "scratch", "memory")

# zipped shapefile, via GDAL's virtual filesystem
QgsVectorLayer("/vsizip//abs/parcels.zip/parcels.shp", "parcels", "ogr")

A delimitedtext URI must be a file:// URL with query parameters β€” passing a bare path there is a very common cause of an invalid layer.

Build database URIs with QgsDataSourceUri

Hand-assembling a PostGIS connection string is error-prone; the helper class formats it correctly.

from qgis.core import QgsDataSourceUri, QgsVectorLayer

uri = QgsDataSourceUri()
uri.setConnection("db.internal", "5432", "gisdb", "gis_user", "secret")
uri.setDataSource("public", "parcels", "geom", aKeyColumn="gid")

layer = QgsVectorLayer(uri.uri(False), "parcels", "postgres")
if not layer.isValid():
    print(layer.dataProvider().error().message())     # names the real cause

Typical provider messages here are unambiguous: authentication failure, no such table, or "no suitable key column", which happens on views without a unique integer column.

Confirm QGIS is initialised

If no provider is registered, every layer is invalid regardless of the URI.

from qgis.core import QgsApplication, QgsProviderRegistry

print("providers:", QgsProviderRegistry.instance().providerList()[:10])

An empty or tiny list means initQgis() has not run, or setPrefixPath() points somewhere without share/qgis.

Check the shapefile's companions

A .shp alone is not a dataset.

from pathlib import Path

shp = Path("data/raw/parcels.shp")
for ext in (".shx", ".dbf", ".prj", ".cpg"):
    companion = shp.with_suffix(ext)
    print(f"{ext}: {'ok' if companion.exists() else 'MISSING'}")

.shx and .dbf are required; a missing .prj loads but leaves the layer without a CRS, which causes different problems downstream.

Code examples

Example 1: a loader that explains every failure

from pathlib import Path
from qgis.core import QgsVectorLayer, QgsRasterLayer, QgsProviderRegistry

def load_layer(source, name=None, provider="ogr", layername=None):
    """Load a layer or raise with everything needed to diagnose the failure."""
    if provider in ("ogr", "gdal") and not str(source).startswith(("/vsi", "http", "postgres")):
        path = Path(source).expanduser().resolve()
        if not path.exists():
            raise FileNotFoundError(f"{path} does not exist (cwd={Path.cwd()})")
        uri = f"{path}|layername={layername}" if layername else str(path)
    else:
        uri = str(source)

    name = name or (layername or Path(str(source)).stem)
    cls = QgsRasterLayer if provider == "gdal" else QgsVectorLayer
    layer = cls(uri, name, provider)

    if not layer.isValid():
        details = []
        dp = layer.dataProvider()
        if dp and dp.error().message():
            details.append(dp.error().message())
        if provider == "ogr" and layername is None:
            subs = QgsProviderRegistry.instance().querySublayers(uri)
            if len(subs) > 1:
                details.append(f"container has {len(subs)} layers: {[s.name() for s in subs]}")
        raise RuntimeError(
            f"invalid layer {uri!r}: " + ("; ".join(details) or "no provider message"))

    return layer

parcels = load_layer("data/raw/atlas.gpkg", layername="parcels")
print(parcels.featureCount(), parcels.crs().authid())

Example 2: validate a whole folder before a batch run

from pathlib import Path
from qgis.core import QgsVectorLayer

def audit(folder: str) -> list[dict]:
    rows = []
    for path in sorted(Path(folder).resolve().rglob("*.gpkg")):
        layer = QgsVectorLayer(str(path), path.stem, "ogr")
        rows.append({
            "file": path.name,
            "valid": layer.isValid(),
            "features": layer.featureCount() if layer.isValid() else None,
            "crs": layer.crs().authid() if layer.isValid() else None,
            "error": "" if layer.isValid() else layer.dataProvider().error().message(),
        })
    return rows

for r in audit("data/raw"):
    flag = "ok " if r["valid"] else "BAD"
    print(f"{flag} {r['file']:<32} {r['features'] or '-':>8}  {r['crs'] or '-':<12} {r['error']}")

Auditing first turns a batch that dies at file 47 into a report you can act on before starting.

Example 3: loading a Processing result safely

import processing
from qgis.core import QgsProcessingContext, QgsProcessingFeedback, QgsVectorLayer

context = QgsProcessingContext()          # keep a reference for the whole run
feedback = QgsProcessingFeedback()

result = processing.run(
    "native:buffer",
    {"INPUT": str(src), "DISTANCE": 50, "OUTPUT": "TEMPORARY_OUTPUT"},
    context=context, feedback=feedback,
)

out = context.getMapLayer(result["OUTPUT"]) or QgsVectorLayer(result["OUTPUT"], "buffered", "ogr")
if not out or not out.isValid():
    raise RuntimeError(f"algorithm produced an invalid output: {result['OUTPUT']}")
print(out.featureCount())

A TEMPORARY_OUTPUT is an id inside the context, not a path β€” fetching it with context.getMapLayer() is why the context must stay alive.

Example 4: a CSV loaded as points

from pathlib import Path
from urllib.parse import quote
from qgis.core import QgsVectorLayer

csv = Path("data/raw/stations.csv").resolve()
uri = (
    f"file:///{quote(str(csv))}"
    "?delimiter=,"
    "&xField=lon&yField=lat"
    "&crs=EPSG:4326"
    "&detectTypes=yes"
    "&spatialIndex=no"
)
layer = QgsVectorLayer(uri, "stations", "delimitedtext")
print(layer.isValid(), layer.featureCount())

If this loads with zero features, the delimiter or the field names are wrong β€” the provider parses the file successfully but finds no coordinates.

Explanation

QgsVectorLayer is a thin object over a provider. Construction does three things: it looks up the provider key in the provider registry, hands it the URI, and asks it to open the source. Any of those can fail, and none of them raise β€” the constructor always returns an object, and validity is a state on it. That design suits an interactive application, where a broken layer appears greyed out in the legend, but it means a script that skips isValid() will fail later, somewhere less informative.

Triage table mapping invalid-layer causes to their fixes.
Seven causes of an invalid layer, and how to tell them apart in one step.

The URI is not a path β€” it is a provider-specific string. For ogr it is a file path optionally followed by |layername= and other pipe-separated options. For delimitedtext it is a file:// URL with query parameters. For postgres it is a key-value connection string. For memory it is a geometry-type specification with fields. Passing a path where a URL is expected fails silently, which is why building URIs with the provided helpers, or with a small function of your own, is worth doing once.

Multi-layer containers add the second frequent cause. A GeoPackage holds any number of layers, and naming only the file leaves the provider to guess. Depending on version and content, it may open the first layer, open nothing, or return a layer that lists sublayers instead of features. Always name the layer explicitly, with the exact case, after listing what the container holds.

Finally, none of this works before initQgis(). The provider registry is populated during application initialisation, so a layer constructed beforehand cannot find any provider at all. That failure looks identical to a wrong path, which is why printing the provider list is a fast way to separate "environment not ready" from "URI wrong".

Edge cases or notes

  • featureCount() returns -1 on an invalid layer: Treat any negative count as "not loaded" rather than "empty".
  • A missing .prj still loads: The layer is valid but layer.crs().isValid() is False. Set it explicitly with layer.setCrs() when you know the true CRS.
  • Case sensitivity in layer names: |layername=Parcels and |layername=parcels are different on Linux. Copy the name from a sublayer listing.
  • File locks: A GeoPackage open in the QGIS GUI can block a writing process on Windows. Close the project or copy the file first.
  • Layers added to QgsProject are owned by it: After addMapLayer(), do not delete the Python object; clear the project instead.
  • /vsicurl/ and cloud sources: Network sources need GDAL_HTTP_* settings and credentials in the environment; a failure here surfaces as an invalid layer, not an HTTP error.
  • The GUI remembers connections, a script does not: A PostGIS layer that works in QGIS may rely on a stored connection and saved credentials. In a script, supply them explicitly.

FAQ

Why does QgsVectorLayer not raise an exception?

Because QGIS is built around an interactive application where a failed layer is shown as broken rather than crashing the program. In a script you have to check isValid() yourself, immediately after construction.

How do I see the real reason a layer is invalid?

Read layer.dataProvider().error().message(), and connect to QgsApplication.messageLog().messageReceived to capture what the provider logged. Between them you almost always get a specific cause.

Do I need |layername= for a GeoPackage?

Yes, whenever the container holds more than one layer. Without it the provider has to guess, and the result varies by version. List the sublayers first and use the exact name.

Which provider key should I use?

ogr for vector files, gdal for rasters, delimitedtext for CSV with coordinates, postgres for PostGIS, memory for scratch layers, wfs/wms for services. The key must match the URI grammar you are using.

Why is featureCount() returning -1?

That is the sentinel for "unknown", returned when the layer is not valid. Check isValid() first; a valid but empty layer returns 0.

Can I load a layer before calling initQgis()?

No. Providers are registered during application initialisation, so every layer built beforehand is invalid. Print QgsProviderRegistry.instance().providerList() to confirm the registry is populated.

Why does the same file load in the QGIS GUI but not in my script?

Usually a relative path, a missing layername, or a saved connection the GUI has and the script does not. Resolve the path to absolute, name the layer explicitly, and supply credentials in the URI.