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

On this page

Problem statement

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

layer = QgsVectorLayer("data/raw/atlas.gpkg|layername=schools", "schools", "ogr")
print(layer.isValid())      # False
print(layer.featureCount()) # a negative number
print(repr(layer.error().message()), repr(layer.dataProvider().error().message()))
False
-2
'' ''

QgsVectorLayer never raises. It returns an object whose isValid() is False, and everything downstream then behaves strangely: a negative feature count, an empty iteration, a Processing algorithm that fails with "Could not load source layer for INPUT". And on QGIS 4.2 the obvious places to ask why β€” the layer's and the provider's error() β€” were empty in every failure on this page. The failure is silent by design, which is why an explicit validity check, and a way to read what the provider did say, are mandatory in every PyQGIS script.

What makes a layer invalid without saying so:

  • the path is relative and the process working directory is not what you assume
  • a multi-layer source (GeoPackage, GeoDatabase, KML) names a layer that is not there
  • the wrong provider key: "ogr" for vectors, "gdal" for rasters, "postgres", "delimitedtext", "memory"
  • a layer was created before QgsApplication, so the plugin providers never registered
  • a missing shapefile sidecar (.shx) or a locked file
  • a CSV given as a bare path where the provider wants a file:// URL
  • a database URI with a wrong port, table or credentials

Every case below was run on QGIS 4.2 against a real file: Rutland's OpenStreetMap points of interest, land use and Land Registry parcels in one GeoPackage, the same points as a shapefile and a CSV, and a PostGIS table of Rutland's wards.

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
  3. read what the provider wrote to the QGIS message log β€” that is where the reason is
  4. list the layers in a container before naming one
  5. make a helper that raises with the URI and those messages, and use it everywhere
from pathlib import Path
from qgis.core import QgsVectorLayer

def load_vector(uri: str, name: str = "layer", provider: str = "ogr") -> QgsVectorLayer:
    with captured_messages() as messages:
        layer = QgsVectorLayer(uri, name, provider)
    if not layer.isValid():
        raise RuntimeError(f"could not load {tidy(uri)!r} β€” " + ("; ".join(messages) or "nothing logged"))
    return layer

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

try:
    load_vector(f"{path}|layername=schools", "schools")
except RuntimeError as error:
    print(error)
531 features, EPSG:27700
could not load 'data/raw/atlas.gpkg|layername=schools' β€” [OGR] Cannot find layer schools.()

captured_messages() (defined with the data below) connects to the QGIS message log for the duration of the call. The provider's own message is the piece almost everyone misses: it 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

Data source URI forms for the common PyQGIS providers. each provider has its own grammarogr β€” GeoPackagefile.gpkg|layername=xogr β€” shapefile/abs/path/file.shpgdal β€” raster/abs/path/dem.tifdelimitedtext β€” CSVfile:///…?xField=lonpostgres β€” PostGISQgsDataSourceUri()memory β€” scratchPoint?crs=EPSG:4326A URI is not a path β€” passing one where the other is expected fails silently spatialworkflow.io
Each provider has its own URI grammar β€” half of all load failures are a malformed one.

Check the file, read the provider log, use the right key

Six checks to run when a PyQGIS layer will not load. six checks, in orderPath resolved to absolute, and existsthe cwd trapisValid() checked immediatelynever raisesQGIS message log readthe real causelayername= given for containersGeoPackageProvider key matches the sourceogr / gdal / …QGIS started before any layerthe registryThe logged message is the piece almost everyone skips spatialworkflow.io
Six checks, in order β€” each rules out one whole class of cause.

Check the file before blaming the API

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

elsewhere = Path("/tmp")                                   # where a scheduler might start the job
print("from /tmp:", (elsewhere / "data/raw/atlas.gpkg").exists())
resolved : data/raw/atlas.gpkg
exists   : True
size     : 22437888
from /tmp: False

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 what the provider logged

error().message() was empty in every failure below, so connect to the message log, which is where the OGR, delimited-text and PostGIS providers explain themselves. On QGIS 4 the signal is messageReceivedWithFormat; the older messageReceived never fired on 4.2.

for label, uri in [
    ("missing file", "data/raw/nope.gpkg"),
    ("unknown layer", "data/raw/atlas.gpkg|layername=schools"),
    ("no .shx", "data/raw/noshx.shp"),
]:
    with captured_messages() as messages:
        layer = QgsVectorLayer(uri, label, "ogr")
    print(f"{label:14} valid={layer.isValid()!s:5} count={layer.featureCount():>3} "
          f"error()={layer.error().message()!r}  log={messages[:1]}")
missing file   valid=False count= -2 error()=''  log=['[OGR] Cannot open data/raw/nope.gpkg ().()']
unknown layer  valid=False count= -2 error()=''  log=['[OGR] Cannot find layer schools.()']
no .shx        valid=False count= -2 error()=''  log=['[OGR] Cannot open data/raw/noshx.shp (Unable to open data/raw/noshx.shx or data/raw/noshx.SHX. Set SHAPE_RESTORE_SHX config option to YES to restore or create it.).(Unable to open data/raw/noshx.shx or data/raw/noshx.SHX. Set SHAPE_RESTORE_SHX config option to YES to restore or create it.)']

The shapefile's message even names the fix: GDAL can rebuild a missing .shx if you set SHAPE_RESTORE_SHX=YES.

List the layers in a container first

A GeoPackage can hold many layers. Naming the file alone does not fail β€” it quietly loads the first layer:

from qgis.core import QgsProviderRegistry, Qgis

whole = QgsVectorLayer("data/raw/atlas.gpkg", "atlas", "ogr")
print("no layername:", whole.isValid(), whole.featureCount(), "features β€” the first layer, whichever that is")

for sub in QgsProviderRegistry.instance().querySublayers("data/raw/atlas.gpkg", Qgis.SublayerQueryFlag.CountFeatures):
    print(f"  {sub.name():8} {sub.featureCount():>6} features  {sub.wkbType().name}")
no layername: True 531 features β€” the first layer, whichever that is
  sites       531 features  Point
  landuse    2405 features  MultiPolygon
  parcels   21253 features  Polygon

Then build the URI with the exact name. In a GeoPackage the match happens in SQLite, where table names are not case-sensitive β€” |layername=Sites loaded the same 531 features here β€” but other formats are stricter, so copy the name from the listing:

uri = f"{Path('data/raw/atlas.gpkg').resolve()}|layername=parcels"
print(QgsVectorLayer(uri, "parcels", "ogr").featureCount(), "parcels")
21253 parcels

Use the right provider key and URI form

from qgis.core import QgsRasterLayer
from urllib.parse import quote

csv = Path("data/raw/stations.csv").resolve()
cases = {
    "vector file (ogr)":         QgsVectorLayer(f"{path}|layername=sites", "sites", "ogr"),
    "vector file, 'gdal' key":   QgsVectorLayer(f"{path}|layername=sites", "sites", "gdal"),
    "raster file (gdal)":        QgsRasterLayer(str(Path("data/raw/dem.tif").resolve()), "dem", "gdal"),
    "CSV as a bare path":        QgsVectorLayer(f"{csv}?delimiter=,&xField=lon&yField=lat&crs=EPSG:4326", "s", "delimitedtext"),
    "CSV as a file:// URL":      QgsVectorLayer(f"file://{quote(str(csv))}?delimiter=,&xField=lon&yField=lat&crs=EPSG:4326", "s", "delimitedtext"),
    "memory layer":              QgsVectorLayer("Point?crs=EPSG:27700&field=id:integer", "scratch", "memory"),
    "zipped shapefile (/vsizip)": QgsVectorLayer(f"/vsizip/{Path('data/raw/sites.zip').resolve()}/sites.shp", "z", "ogr"),
}
for label, layer in cases.items():
    count = layer.featureCount() if hasattr(layer, "featureCount") else f"{layer.width()}x{layer.height()}"
    print(f"{label:28} valid={layer.isValid()!s:5} {count}")
vector file (ogr)            valid=True  531
vector file, 'gdal' key      valid=False -1
raster file (gdal)           valid=True  933x933
CSV as a bare path           valid=False 4047647854835025186
CSV as a file:// URL         valid=True  531
memory layer                 valid=True  0
zipped shapefile (/vsizip)   valid=True  531

A delimitedtext URI must be a file:// URL with query parameters β€” a bare path fails with File cannot be opened or delimiter parameters are not valid in the log. And do not read featureCount() on an invalid delimited-text layer as a sentinel: in trial runs it returned 0 once and a meaningless 15-digit number another time. Test isValid(). A wrong key is quieter still: "gdal" for a vector file creates no provider at all.

Which Rutland layers QGIS 4.2 loaded, by provider key, URI form and missing shapefile sidecar. what isValid() said, and what it missedsourceisValid()what you actually getGeoPackage, no layernameTruethe first layer (sites)vector file, "gdal" keyFalseno provider at allCSV as a bare pathFalsenothingshapefile, no .shxFalsenothingshapefile, no .dbfTrue531 points, no attributesshapefile, no .prjTrue531 points, no CRSThe three that load are the dangerous ones spatialworkflow.io
Measured on this page under QGIS 4.2. isValid() is necessary, not sufficient.

Build database URIs with QgsDataSourceUri

Hand-assembling a PostGIS connection string is error-prone; the helper class formats it correctly. Here is the same table with three things wrong and then right:

from qgis.core import QgsDataSourceUri

def postgis_layer(password="guide", port="55433", table="rutland_wards"):
    uri = QgsDataSourceUri()
    uri.setConnection("localhost", port, "gis", "user", password)
    uri.setDataSource("public", table, "geom", aKeyColumn="ogc_fid")
    with captured_messages() as messages:
        layer = QgsVectorLayer(uri.uri(False), "wards", "postgres")
    return layer, messages

for label, kwargs in [("wrong password", {"password": "wrong"}), ("wrong port", {"port": "55499"}),
                      ("no such table", {"table": "no_table"}), ("correct", {})]:
    layer, messages = postgis_layer(**kwargs)
    reason = next((m.strip() for text in messages for m in text.split("\n")
                   if any(k in m for k in ("FATAL", "refused", "does not exist"))), "")
    print(f"{label:15} valid={layer.isValid()!s:5} count={layer.featureCount():>3}  {reason[:95]}")
wrong password  valid=False count=  0  connection to server at "localhost" (127.0.0.1), port 55433 failed: FATAL:  password authentica
wrong port      valid=False count=  0  connection to server at "localhost" (127.0.0.1), port 55499 failed: Connection refused
no such table   valid=False count=  0  [PostGIS] Erroneous query: SELECT has_table_privilege('"public"."no_table"','SELECT'),pg_is_in_
correct         valid=True  count= 15  

The provider's messages are specific β€” password authentication failed, connection refused, an erroneous query naming "public"."no_table" β€” but only in the log.

Start QGIS before you make any layer

The earlier examples all loaded β€” and so would a GeoPackage layer created before QgsApplication exists. That is the trap: the first layer made too early builds the provider registry without the plugin providers, and it stays that way for the rest of the process.

%%writefile too_early.py
import os
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from pathlib import Path
from urllib.parse import quote
from qgis.core import QgsApplication, QgsProviderRegistry, QgsVectorLayer

early = QgsVectorLayer("data/raw/atlas.gpkg|layername=sites", "early", "ogr")     # before the application
print("GeoPackage made before QgsApplication:", early.isValid())

qgs = QgsApplication([], False)
qgs.initQgis()
providers = QgsProviderRegistry.instance().providerList()
csv = QgsVectorLayer(f"file://{quote(str(Path('data/raw/stations.csv').resolve()))}"
                     "?delimiter=,&xField=lon&yField=lat&crs=EPSG:4326", "csv", "delimitedtext")
print(len(providers), "providers; delimitedtext:", "delimitedtext" in providers, "postgres:", "postgres" in providers)
print("CSV layer after initQgis():", csv.isValid())
qgs.exitQgis()
import subprocess, sys
run = subprocess.run([sys.executable, "too_early.py"], capture_output=True, text=True)
print(run.stdout.strip())
print("in this session:", len(QgsProviderRegistry.instance().providerList()), "providers")
GeoPackage made before QgsApplication: True
17 providers; delimitedtext: False postgres: False
CSV layer after initQgis(): False
in this session: 34 providers

The GeoPackage loaded, the CSV β€” made correctly, after initQgis() β€” did not, and nothing said why. Start the application first, before any layer, and print providerList() when a provider-specific layer fails for no visible reason.

Check the shapefile's companions

A .shp alone is not a dataset.

for name in ("noshx", "nodbf", "noprj"):
    shp = Path(f"data/raw/{name}.shp")
    missing = [ext for ext in (".shx", ".dbf", ".prj", ".cpg") if not shp.with_suffix(ext).exists()]
    layer = QgsVectorLayer(str(shp), name, "ogr")
    print(f"{name:6} missing {missing}: valid={layer.isValid()!s:5} count={layer.featureCount():>4} "
          f"fields={layer.fields().names() if layer.isValid() else '-'} crs={layer.crs().authid() or 'none'}")
noshx  missing ['.shx', '.cpg']: valid=False count=  -2 fields=- crs=none
nodbf  missing ['.dbf', '.cpg']: valid=True  count= 531 fields=[] crs=EPSG:27700
noprj  missing ['.prj', '.cpg']: valid=True  count= 531 fields=['osm_id', 'fclass', 'name'] crs=none

Only the .shx is fatal. A missing .dbf loads the geometry with no attributes at all, and a missing .prj loads a layer with no CRS β€” both valid, both wrong downstream.

A loader that explains every failure

The data used on this page

Rutland from open data: OpenStreetMap points of interest and land use from Geofabrik's 1 January 2026 extract (Β© OpenStreetMap contributors, ODbL) and HM Land Registry's freehold parcels (Open Government Licence), written as three layers β€” sites, landuse, parcels β€” of data/raw/atlas.gpkg; the points again as a shapefile (whole, zipped, and three copies each missing one sidecar) and as stations.csv with longitude and latitude; a Copernicus 30 m DEM clip; and Rutland's 15 ONS wards loaded into a throwaway PostGIS in Docker on port 55433. captured_messages() collects what QGIS logs during a block, and tidy() shortens paths for printing.

import csv
import os
import shutil
import time
import zipfile
from contextlib import contextmanager
from pathlib import Path

import requests
from osgeo import gdal, ogr, osr

gdal.UseExceptions()
raw = Path("data/raw")
raw.mkdir(parents=True, exist_ok=True)
if not (raw / "atlas.gpkg").exists():
    osm = raw / "rutland.shp.zip"
    osm.write_bytes(requests.get(
        "https://download.geofabrik.de/europe/united-kingdom/england/rutland-260101-free.shp.zip", timeout=300).content)
    reply = requests.Session().get(
        "https://use-land-property-data.service.gov.uk/datasets/inspire/download/Rutland.zip", timeout=300)
    reply.raise_for_status()
    (raw / "Rutland.zip").write_bytes(reply.content)
    to_bng = dict(dstSRS="EPSG:27700", reproject=True)
    gdal.VectorTranslate(str(raw / "atlas.gpkg"), f"/vsizip/{osm}/gis_osm_pois_free_1.shp", layerName="sites",
                         selectFields=["osm_id", "fclass", "name"], **to_bng)
    gdal.VectorTranslate(str(raw / "atlas.gpkg"), f"/vsizip/{osm}/gis_osm_landuse_a_free_1.shp", layerName="landuse",
                         accessMode="append", geometryType="MULTIPOLYGON", **to_bng)
    gdal.VectorTranslate(str(raw / "atlas.gpkg"), f"/vsizip/{raw}/Rutland.zip/Land_Registry_Cadastral_Parcels.gml",
                         layerName="parcels", accessMode="append")
    gdal.VectorTranslate(str(raw / "sites.shp"), str(raw / "atlas.gpkg"), layers=["sites"])
    with zipfile.ZipFile(raw / "sites.zip", "w") as z:
        for sidecar in sorted(raw.glob("sites.*")):
            if sidecar.suffix != ".zip":
                z.write(sidecar, sidecar.name)
    for dropped, name in (("shx", "noshx"), ("dbf", "nodbf"), ("prj", "noprj")):
        for sidecar in raw.glob("sites.*"):
            if sidecar.suffix not in (f".{dropped}", ".zip"):
                shutil.copy(sidecar, raw / f"{name}{sidecar.suffix}")
    source = ogr.Open(str(raw / "atlas.gpkg"))                 # keep the dataset alive while reading
    layer = source.GetLayer("sites")
    wgs84 = osr.SpatialReference(); wgs84.ImportFromEPSG(4326)
    wgs84.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER)
    to_wgs84 = osr.CoordinateTransformation(layer.GetSpatialRef(), wgs84)
    with open(raw / "stations.csv", "w", newline="") as fh:
        out = csv.writer(fh)
        out.writerow(["name", "fclass", "lon", "lat"])
        for feature in layer:
            point = feature.GetGeometryRef().Clone()
            point.Transform(to_wgs84)
            out.writerow([feature.GetField("name"), feature.GetField("fclass"), round(point.GetX(), 6), round(point.GetY(), 6)])
    gdal.Warp(str(raw / "dem.tif"), "/vsicurl/https://copernicus-dem-30m.s3.amazonaws.com/"
              "Copernicus_DSM_COG_10_N52_00_W001_00_DEM/Copernicus_DSM_COG_10_N52_00_W001_00_DEM.tif",
              dstSRS="EPSG:27700", outputBounds=(479000, 292000, 507000, 320000), xRes=30, yRes=30)
    (raw / "wards.geojson").write_bytes(requests.get(
        "https://services1.arcgis.com/ESMARspQHYMw9BZ9/arcgis/rest/services/"
        "Wards_December_2024_Boundaries_UK_BGC/FeatureServer/0/query",
        params={"where": "LAD24CD='E06000017'", "outFields": "WD24CD,WD24NM", "outSR": 27700, "f": "geojson"},
        timeout=120).content)

if os.system("docker inspect guide-postgis >/dev/null 2>&1") != 0:
    os.system("docker run -d --name guide-postgis -e POSTGRES_USER=user -e POSTGRES_PASSWORD=guide "
              "-e POSTGRES_DB=gis -p 127.0.0.1:55433:5432 postgis/postgis:16-3.4 >/dev/null")
    time.sleep(10)
gdal.VectorTranslate("PG:host=localhost port=55433 dbname=gis user=user password=guide",
                     str(raw / "wards.geojson"), layerName="rutland_wards", accessMode="overwrite",
                     layerCreationOptions=["GEOMETRY_NAME=geom"])

os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from qgis.core import QgsApplication, Qgis, QgsVectorLayer
qgs = QgsApplication([], False)
qgs.initQgis()

HERE = str(Path.cwd())


def tidy(text):
    return str(text).replace(HERE + "/", "").replace(HERE, ".")


@contextmanager
def captured_messages():
    """Collect what QGIS logs while the block runs."""
    messages = []
    def on_message(message, tag, level, text_format=None):
        messages.append(f"[{tag}] {tidy(message)}")
    log = QgsApplication.messageLog()
    log.messageReceivedWithFormat.connect(on_message)
    try:
        yield messages
    finally:
        QgsApplication.processEvents()
        log.messageReceivedWithFormat.disconnect(on_message)


print("QGIS", Qgis.QGIS_VERSION, "|", sorted(p.name for p in raw.iterdir() if p.suffix in (".gpkg", ".shp", ".csv", ".tif")))
QGIS 4.2.2-BelΓ©m do ParΓ‘ | ['atlas.gpkg', 'dem.tif', 'nodbf.shp', 'noprj.shp', 'noshx.shp', 'sites.shp', 'stations.csv']

Example 1: a loader that explains every failure

from qgis.core import QgsRasterLayer, QgsProviderRegistry, Qgis

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", "PG:")):
        path = Path(source).expanduser().resolve()
        if not path.exists():
            raise FileNotFoundError(f"{tidy(path)} does not exist (cwd={tidy(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
    with captured_messages() as messages:
        layer = cls(uri, name, provider)

    if not layer.isValid():
        details = list(messages)
        if provider == "ogr":
            base = uri.split("|")[0]
            subs = QgsProviderRegistry.instance().querySublayers(base)
            if len(subs) > 1:
                details.append(f"container has {len(subs)} layers: {[s.name() for s in subs]}")
        raise RuntimeError(f"invalid layer {tidy(uri)!r}: " + ("; ".join(details) or "nothing logged"))
    return layer

parcels = load_layer("data/raw/atlas.gpkg", layername="parcels")
print(parcels.featureCount(), parcels.crs().authid())
for kwargs in ({"source": "data/raw/atlas.gpkg", "layername": "schools"},
               {"source": "data/raw/atlass.gpkg", "layername": "parcels"}):
    try:
        load_layer(**kwargs)
    except (RuntimeError, FileNotFoundError) as error:
        print(type(error).__name__ + ":", error)
21253 EPSG:27700
RuntimeError: invalid layer 'data/raw/atlas.gpkg|layername=schools': [OGR] Cannot find layer schools.(); container has 3 layers: ['sites', 'landuse', 'parcels']
FileNotFoundError: data/raw/atlass.gpkg does not exist (cwd=.)

Example 2: validate a whole folder before a batch run

def audit(folder: str) -> list[dict]:
    rows = []
    for path in sorted(Path(folder).resolve().glob("*.shp")):
        with captured_messages() as messages:
            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,
            "fields": len(layer.fields()) if layer.isValid() else None,
            "error": "" if layer.isValid() else (messages[0][:70] if messages else "nothing logged"),
        })
    return rows

for r in audit("data/raw"):
    flag = "ok " if r["valid"] else "BAD"
    print(f"{flag} {r['file']:<12} {r['features'] or '-':>5}  {r['crs'] or '-':<11} fields={r['fields']!s:4} {r['error']}")
ok  nodbf.shp      531  EPSG:27700  fields=0    
ok  noprj.shp      531  -           fields=3    
BAD noshx.shp        -  -           fields=None [OGR] Cannot open data/raw/noshx.shp (Unable to open data/raw/noshx.sh
ok  sites.shp      531  EPSG:27700  fields=3    

Auditing first turns a batch that dies at file 47 into a report you can act on before starting β€” including the files that load but should not be trusted, like the one with no CRS or no fields.

Example 3: loading a Processing result safely

import processing
from processing.core.Processing import Processing
from qgis.core import QgsProcessingContext, QgsProcessingFeedback, QgsProcessingException
Processing.initialize()

context = QgsProcessingContext()
feedback = QgsProcessingFeedback()
result = processing.run(
    "native:buffer",
    {"INPUT": str(Path("data/raw/atlas.gpkg").resolve()) + "|layername=sites", "DISTANCE": 50,
     "OUTPUT": "TEMPORARY_OUTPUT"},
    context=context, feedback=feedback,
)
out = result["OUTPUT"]
print(type(out).__name__, out.isValid(), out.featureCount())

try:
    processing.run("native:buffer", {"INPUT": "data/raw/atlas.gpkg|layername=schools", "DISTANCE": 50,
                                     "OUTPUT": "TEMPORARY_OUTPUT"})
except QgsProcessingException as error:
    print("QgsProcessingException:", " ".join(str(error).split()))
QgsVectorLayer True 531
QgsProcessingException: Unable to execute algorithm Could not load source layer for INPUT: data/raw/atlas.gpkg|layername=schools not found

From processing.run, a TEMPORARY_OUTPUT comes back as the layer object itself; keep a reference to it for as long as you use it. An invalid input, by contrast, does raise β€” Processing validates its sources, which is one reason to hand layers to algorithms by URI when you are unsure of them.

Example 4: a CSV loaded as points

from urllib.parse import quote

csv_path = Path("data/raw/stations.csv").resolve()
def csv_layer(delimiter=",", x="lon", y="lat"):
    uri = (f"file://{quote(str(csv_path))}?delimiter={delimiter}&xField={x}&yField={y}"
           "&crs=EPSG:4326&detectTypes=yes&spatialIndex=no")
    with captured_messages() as messages:
        layer = QgsVectorLayer(uri, "stations", "delimitedtext")
    return layer, messages

for label, kwargs in [("correct", {}), ("wrong field names", {"x": "x", "y": "y"}),
                      ("wrong delimiter", {"delimiter": ";"})]:
    layer, messages = csv_layer(**kwargs)
    count = layer.featureCount() if layer.isValid() else "n/a"
    print(f"{label:18} valid={layer.isValid()!s:5} count={count!s:>4}  {messages[-1] if messages else ''}")
correct            valid=True  count= 531  
wrong field names  valid=False count= n/a  [DelimitedText] Y field y is not defined in delimited text file
wrong delimiter    valid=False count= n/a  [DelimitedText] Y field lat is not defined in delimited text file

The wrong delimiter is the instructive one: the provider reads each line as a single column, cannot find the coordinate fields, and reports a field β€” not the delimiter β€” as the problem.

The page's QGIS session ends here:

from qgis.core import QgsProject
QgsProject.instance().clear()
qgs.exitQgis()
print("QGIS closed")
QGIS closed

What constructing a layer actually does

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 and the message log panel says why, but it means a script that skips isValid(), and never looks at the log, fails later somewhere less informative.

Seven causes of an invalid PyQGIS layer, the message QGIS 4.2 logged for each, and the fix. isValid() is False β€” but whyCause (what the log said)FixRelative path ("Cannot open …")Pass str(Path(p).resolve())Wrong layername ("Cannot findlayer")List sublayers, copy the nameWrong provider key (no provider)ogr for vector, gdal for rasterMissing .shx sidecarRestore, or SHAPE_RESTORE_SHX=YESCSV given as a bare pathBuild a delimitedtext file:// URLPostGIS password, port or tableRead the [PostGIS] log messageA layer made before QgsApplicationStart QGIS before any layererror().message() was empty every time; the message log was not spatialworkflow.io
Seven causes of an invalid layer, each measured on QGIS 4.2, and the message that tells them apart.

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 does not fail β€” QGIS 4.2 opened the first one. That is worse than an error when the first layer is not the one you meant. Always name the layer explicitly, after listing what the container holds.

Finally, the order of start-up matters more than it looks. The provider registry is built the first time anything asks for it. If that is a layer created before QgsApplication exists, the registry is built without the plugin path, and only the providers compiled into the core library are there β€” ogr, gdal and memory among them, delimitedtext and postgres not. The GeoPackage then works and the CSV and PostGIS layers do not, for the rest of the process, with nothing in the log to connect the two.

Negative counts, missing sidecars and file locks

  • featureCount() is negative on an invalid layer: -2 for an OGR layer that failed to open, -1 when no provider was created. Treat any negative count as "not loaded"; a valid, empty layer returns 0.
  • A missing .prj or .dbf still loads: The layer is valid but has no CRS, or no attributes. Check layer.crs().isValid() and the field list, not just isValid().
  • Layer names: GeoPackage matched |layername=Sites to sites because SQLite table names are case-insensitive. Other formats are not so forgiving β€” copy the name from a sublayer listing.
  • File locks: A GeoPackage open in the QGIS GUI can block a writing process, especially 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 and a log message, 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.
  • A table with no primary key still loads: the PostGIS provider falls back to ctid and logs that editing is disabled. Name the key with aKeyColumn when there is one.

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?

Connect to the QGIS message log β€” QgsApplication.messageLog().messageReceivedWithFormat on QGIS 4 β€” for the duration of the load. On QGIS 4.2 layer.error() and dataProvider().error() were empty for every failure on this page, while the log said Cannot find layer schools, Connection refused or X field x is not defined.

Do I need |layername= for a GeoPackage?

Yes, whenever the container holds more than one layer. Without it QGIS 4.2 did not fail β€” it silently opened the first layer. 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?

It is a sentinel, not a count: QGIS 4.2 returned -2 for an OGR layer that could not open and -1 when no provider was created at all. Check isValid() first; a valid but empty layer returns 0.

Can I load a layer before calling initQgis()?

Do not. A GeoPackage layer made before QgsApplication loaded on QGIS 4.2 β€” but it built the provider registry without the plugin providers, so a correctly written CSV layer made after initQgis() in the same process was invalid, with nothing logged. Start the application before any layer, and print QgsProviderRegistry.instance().providerList() if in doubt.

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.