QGIS Data Providers Explained: How QGIS Reads Any Data Source
Problem statement
Three lines that look almost identical and behave completely differently:
QgsVectorLayer("/data/parcels.gpkg", "parcels", "ogr")
QgsVectorLayer("Point?crs=EPSG:27700&field=id:integer", "scratch", "memory")
QgsVectorLayer("dbname='gis' host=db.internal table=\"public\".\"parcels\" (geom)", "p", "postgres")
The third argument is the provider key, and it decides how the first argument is interpreted. Get it wrong and you get an invalid layer with no exception β the single most common PyQGIS confusion.
Providers are also the reason a PostGIS layer stays live while a shapefile is a snapshot, why some layers can be edited and others cannot, why setSubsetString sometimes runs on a server, and why the same layer behaves differently in the canvas depending on where it came from.
Quick answer
A provider is the plugin that connects a layer to a data source:
- The provider key β
ogr,gdal,postgres,memory,delimitedtext,wfs,wmsβ selects the implementation - The URI is provider-specific: a path for
ogr, a URL fordelimitedtext, a key-value string forpostgres - Providers declare capabilities: can they add features, change attributes, do they support transactions?
- Database providers keep the layer live; file providers read on demand and cache
QgsVectorLayernever raises β always checkisValid()and readdataProvider().error().message()
from qgis.core import QgsProviderRegistry, QgsVectorLayer
print(sorted(QgsProviderRegistry.instance().providerList()))
# ['DB2', 'WFS', 'arcgisfeatureserver', 'delimitedtext', 'gdal', 'gpx', 'memory',
# 'mesh_memory', 'mssql', 'ogr', 'oracle', 'postgres', 'spatialite', 'virtual', 'wms', ...]
layer = QgsVectorLayer("/data/parcels.gpkg|layername=parcels", "parcels", "ogr")
print(layer.isValid(), layer.providerType(), layer.dataProvider().name())
If a layer is invalid, the provider knows why β layer.dataProvider().error().message() is the message worth reading before anything else.
One layer class, many providers
Step-by-step solution
The provider key decides everything
from qgis.core import QgsVectorLayer, QgsRasterLayer
# ogr β the general file provider, backed by GDAL's vector half
QgsVectorLayer("/abs/parcels.gpkg|layername=parcels", "parcels", "ogr")
QgsVectorLayer("/abs/roads.shp", "roads", "ogr")
QgsVectorLayer("/vsizip//abs/delivery.zip/parcels.shp", "zipped", "ogr")
# gdal β the raster provider
QgsRasterLayer("/abs/dem.tif", "dem", "gdal")
# delimitedtext β a CSV with coordinates, as a file:// URL with query parameters
QgsVectorLayer(
"file:///abs/stations.csv?delimiter=,&xField=lon&yField=lat&crs=EPSG:4326",
"stations", "delimitedtext")
# memory β a scratch layer defined by its geometry type and fields
QgsVectorLayer("Point?crs=EPSG:27700&field=id:integer&field=name:string(80)",
"scratch", "memory")
# postgres β a connection string, best built with QgsDataSourceUri
QgsVectorLayer(uri.uri(False), "parcels", "postgres")
# wfs β a remote service
QgsVectorLayer("pagingEnabled='true' url='https://example.org/wfs' typename='ns:parcels'",
"remote", "WFS")
Passing a bare path with the delimitedtext key, or a file:// URL with ogr, produces an invalid layer with no explanation β the URI grammar belongs to the provider.
Capabilities: what a provider can actually do
from qgis.core import QgsVectorDataProvider
def describe_capabilities(layer) -> dict:
provider = layer.dataProvider()
caps = provider.capabilities()
checks = {
"add features": QgsVectorDataProvider.AddFeatures,
"delete features": QgsVectorDataProvider.DeleteFeatures,
"change attributes": QgsVectorDataProvider.ChangeAttributeValues,
"change geometries": QgsVectorDataProvider.ChangeGeometries,
"add attributes": QgsVectorDataProvider.AddAttributes,
"delete attributes": QgsVectorDataProvider.DeleteAttributes,
"select at id": QgsVectorDataProvider.SelectAtId,
"transactions": QgsVectorDataProvider.TransactionSupport,
"fast truncate": QgsVectorDataProvider.FastTruncate,
}
return {name: bool(caps & flag) for name, flag in checks.items()}
for name, able in describe_capabilities(layer).items():
print(f" {name:20} {'yes' if able else 'no'}")
This is how a plugin decides whether to offer an "edit" button, and how your script should decide whether an edit is even possible. A WFS layer without transactions, a read-only CSV, or a file on a read-only mount will all decline edits β checking first turns a silent False into a clear message.
Live sources versus snapshots
import time
from qgis.core import QgsVectorLayer
pg = QgsVectorLayer(uri.uri(False), "parcels", "postgres")
print(pg.featureCount()) # queried from the database
# someone inserts rows elsewhere...
pg.reload()
print(pg.featureCount()) # updated
gpkg = QgsVectorLayer("/data/parcels.gpkg|layername=parcels", "parcels", "ogr")
# the file provider caches metadata; reload() re-reads it
gpkg.reload()
A database-backed layer is a view: features are fetched as the canvas or an iterator asks for them, and a filter can be pushed down to the server. A file-backed layer reads from disk on demand but caches extent, feature count and field schema, which is why an externally modified file needs reload().
Filters: pushed down or evaluated locally
# setSubsetString is a provider-level filter
layer.setSubsetString('"class" = \'residential\' AND area_m2 > 500')
print(layer.featureCount()) # reflects the filter
layer.setSubsetString("") # clear it
For the postgres provider this becomes a SQL WHERE clause and the rows never leave the server. For ogr on a GeoPackage it becomes an OGR attribute filter, which the driver can often satisfy with an index. For a CSV it is evaluated row by row. Same API, very different cost β and knowing which you have explains a lot about performance.
The provider connection API
Modern QGIS exposes a uniform API for browsing and managing connections, independent of the provider:
from qgis.core import QgsProviderRegistry
metadata = QgsProviderRegistry.instance().providerMetadata("postgres")
connection = metadata.createConnection(uri.uri(False), {})
print(connection.schemas())
for table in connection.tables("public"):
print(f" {table.tableName():24} {table.geometryColumnTypes()}")
connection.executeSql("CREATE SCHEMA IF NOT EXISTS results")
rows = connection.executeSql("SELECT class, count(*) FROM parcels GROUP BY class")
The same code shape works for ogr (browsing a GeoPackage's layers), spatialite and other providers that implement the interface. It is what the QGIS Browser panel uses, and it is far cleaner than assembling driver-specific calls.
Diagnosing an invalid layer
from qgis.core import QgsVectorLayer, QgsProviderRegistry
def diagnose(uri: str, name: str, provider: str):
if provider not in QgsProviderRegistry.instance().providerList():
return f"provider {provider!r} is not registered β is QGIS initialised?"
layer = QgsVectorLayer(uri, name, provider)
if layer.isValid():
return (f"ok: {layer.featureCount()} features, {layer.crs().authid()}, "
f"{[f.name() for f in layer.fields()][:6]}")
dp = layer.dataProvider()
if dp is None:
return "no provider instance was created β check the provider key"
return f"invalid: {dp.error().message() or 'no message from the provider'}"
print(diagnose("/data/parcels.gpkg|layername=parcels", "parcels", "ogr"))
print(diagnose("/data/parcels.gpkg", "wrong-key", "postgres"))
Three outcomes, three different causes: unknown provider key, no provider instance, or a provider that tried and failed. Only the last one has a message worth reading, which is exactly why so many people see none.
Sublayers: one source, many layers
from qgis.core import QgsProviderRegistry
parts = QgsProviderRegistry.instance().querySublayers("/data/atlas.gpkg")
for sub in parts:
print(f"{sub.name():20} {sub.wkbType()} {sub.uri()}")
querySublayers is the provider-agnostic way to ask "what is in this file?" β it works for GeoPackage, File Geodatabase, NetCDF, and anything else that can hold several datasets. Using it before constructing a layer removes the guesswork about |layername=.
Code examples
Example 1: a loader that speaks every provider
from pathlib import Path
from urllib.parse import quote
from qgis.core import QgsVectorLayer, QgsRasterLayer, QgsDataSourceUri
def load(source, name=None, kind="auto", **options) -> QgsVectorLayer | QgsRasterLayer:
"""Build the right URI for the right provider, and fail with a real message."""
if kind == "auto":
text = str(source).lower()
if text.endswith((".tif", ".tiff", ".img", ".vrt")):
kind = "raster"
elif text.endswith(".csv"):
kind = "csv"
elif text.startswith(("postgres", "dbname")):
kind = "postgres"
else:
kind = "vector"
if kind == "raster":
layer = QgsRasterLayer(str(Path(source).resolve()), name or Path(source).stem, "gdal")
elif kind == "csv":
path = Path(source).resolve()
uri = (f"file:///{quote(str(path))}?delimiter={options.get('delimiter', ',')}"
f"&xField={options.get('x', 'lon')}&yField={options.get('y', 'lat')}"
f"&crs={options.get('crs', 'EPSG:4326')}&detectTypes=yes")
layer = QgsVectorLayer(uri, name or path.stem, "delimitedtext")
elif kind == "postgres":
layer = QgsVectorLayer(str(source), name or "postgres layer", "postgres")
else:
path = Path(str(source).split("|")[0]).resolve()
uri = f"{path}|layername={options['layer']}" if options.get("layer") else str(path)
layer = QgsVectorLayer(uri, name or path.stem, "ogr")
if not layer.isValid():
dp = layer.dataProvider()
raise RuntimeError(
f"could not load {source!r} as {kind}: "
f"{dp.error().message() if dp else 'no provider created'}")
return layer
Example 2: check before you edit
from qgis.core import QgsVectorDataProvider
def can_edit(layer) -> tuple[bool, str]:
provider = layer.dataProvider()
if provider is None:
return False, "no provider"
caps = provider.capabilities()
needed = {
"add": QgsVectorDataProvider.AddFeatures,
"change attributes": QgsVectorDataProvider.ChangeAttributeValues,
"change geometry": QgsVectorDataProvider.ChangeGeometries,
}
missing = [name for name, flag in needed.items() if not caps & flag]
if missing:
return False, f"{layer.providerType()} cannot: {', '.join(missing)}"
if layer.readOnly():
return False, "layer is marked read-only"
return True, "editable"
ok, why = can_edit(layer)
print(f"{layer.name()}: {why}")
Example 3: build a scratch layer in memory
from qgis.core import QgsVectorLayer, QgsFeature, QgsGeometry, QgsPointXY, QgsField
from qgis.PyQt.QtCore import QVariant
# the memory provider's URI *is* the schema
layer = QgsVectorLayer(
"Polygon?crs=EPSG:27700"
"&field=id:integer"
"&field=name:string(80)"
"&field=area_m2:double(12,2)"
"&index=yes",
"scratch", "memory")
print(layer.isValid(), [f.name() for f in layer.fields()])
feature = QgsFeature(layer.fields())
feature.setGeometry(QgsGeometry.fromPolygonXY([[
QgsPointXY(0, 0), QgsPointXY(100, 0), QgsPointXY(100, 100), QgsPointXY(0, 100)]]))
feature["id"], feature["name"] = 1, "test plot"
layer.dataProvider().addFeatures([feature])
layer.updateExtents()
print(layer.featureCount(), layer.extent().toString(0))
Memory layers are the natural staging area for constructed features, algorithm outputs and test fixtures β no file, no cleanup, full API.
Example 4: browse any source with the connection API
from qgis.core import QgsProviderRegistry, QgsProviderConnectionException
def browse(provider_key: str, uri: str):
metadata = QgsProviderRegistry.instance().providerMetadata(provider_key)
if metadata is None:
raise ValueError(f"no such provider: {provider_key}")
try:
connection = metadata.createConnection(uri, {})
except QgsProviderConnectionException as exc:
raise RuntimeError(f"cannot connect: {exc}") from exc
try:
schemas = connection.schemas()
except Exception:
schemas = [""] # file-based providers have no schemas
for schema in schemas:
for table in connection.tables(schema):
print(f"{schema or '-':10} {table.tableName():28} "
f"{table.geometryColumnTypes() or 'no geometry'}")
browse("ogr", "/data/atlas.gpkg")
browse("postgres", "dbname='gis' host='db.internal' user='gis'")
One function, two very different sources β which is exactly the abstraction providers exist to give you.
Explanation
QGIS separates what a layer is from where its data comes from. QgsVectorLayer handles rendering, styling, selection, the edit buffer and the layer tree; a QgsVectorDataProvider handles fetching features, reporting the schema, and applying changes. Everything a layer can do that involves data goes through its provider.
That separation is what lets one application read a shapefile, a PostGIS table, a WFS service and a CSV with the same code paths for symbology, labelling and analysis. It also explains the differences you notice in use. A postgres layer can push a filter into SQL, can participate in transactions, and reflects changes made by other users; an ogr layer over a GeoPackage caches its feature count and needs reload(); a wfs layer pages requests over HTTP and may not support editing at all.
Capabilities are how a provider advertises those differences. Rather than the application knowing which sources are editable, each provider reports a bitmask of what it supports, and the UI enables or greys out actions accordingly. In a script the same flags let you check before attempting an edit that will otherwise fail with a bare False.
The URI is the other half of the contract, and it is where most confusion lives, because each provider defines its own grammar. ogr takes a path with optional pipe-separated options. delimitedtext takes a file:// URL with query parameters. postgres takes a key-value connection string, which is why QgsDataSourceUri exists β hand-assembly gets the quoting wrong. memory takes a schema definition, which is genuinely elegant: the URI is the layer.
Finally, provider registration is why nothing works before initQgis(). The registry is populated during application initialisation; before that, QgsProviderRegistry.instance().providerList() is empty and every layer you construct is invalid, regardless of how correct its URI is. Printing that list is the fastest way to distinguish "my environment is not ready" from "my URI is wrong" β two problems with identical symptoms.
Edge cases or notes
ogris not one format: It is GDAL's whole vector driver set behind one key, so its behaviour varies by file type.QgsVectorLayernever raises: Always checkisValid(); the constructor returns an object either way.- Provider keys are case-sensitive in places:
WFSandwmsdiffer in capitalisation historically β copy them exactly. setSubsetStringis not the same as a feature request: It changes the layer persistently, including its feature count and extent.- Memory layers vanish with the project: They are not saved into a
.qgzunless the project's "save memory layers" option is on. - A GeoPackage open in the GUI can block a writer: SQLite locking, especially on Windows.
querySublayersreplaces older sublayer APIs: It is provider-agnostic and the right way to enumerate a container.
Internal links
- PyQGIS Layer Fails to Load (isValid() Returns False): How to Fix It
- How to Load and Write PostGIS Layers from PyQGIS
- How to Add, Edit and Delete Features with PyQGIS
- How to Automate QGIS with Python (PyQGIS): The Complete Workflow
- The PyQGIS API Map: Which Class Does What
- What GDAL and OGR Actually Are (and Why Everything Depends on Them)
FAQ
What is a QGIS data provider?
The component that connects a layer to its data source. It fetches features, reports the schema and CRS, applies edits, and declares what it is capable of. The provider key is the third argument to QgsVectorLayer.
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.
Why is my layer invalid with the right path?
Often the wrong provider key or the wrong URI grammar for that provider β a bare path passed to delimitedtext, or a missing |layername= on a multi-layer GeoPackage. Read dataProvider().error().message().
How do I know whether a layer can be edited?
Check layer.dataProvider().capabilities() against the flags you need. Some providers are read-only, and a WFS layer may not support transactions.
What is the difference between reload() and triggerRepaint()?
reload() asks the provider to re-read the source, which is what you need after an external change. triggerRepaint() only redraws what is already loaded.
Does setSubsetString run on the server?
For postgres it becomes a SQL WHERE clause, so yes. For ogr it becomes an attribute filter that the driver may satisfy with an index. For a CSV it is evaluated row by row.
Why is the provider list empty in my script?
Because QgsApplication.initQgis() has not run. Providers are registered during application initialisation, and until then every layer is invalid whatever its URI.