Decode and Build Data Source URIs in PyQGIS
Every layer in QGIS is a provider key plus a source string. For a shapefile the string is a path; for a GeoPackage it is a path with |layername= glued on; for PostGIS it is a run of key='value' pairs; for a WMS it is URL-encoded query parameters. Scripts that need to know which file a layer reads, move a project to a new server or swap one table for another usually start by splitting that string with split("|") — and break the first time a path contains a pipe, a quote or a space.
The provider registry already knows how to parse and assemble each provider's format. This recipe belongs to Layer Data Sources & Formats in PyQGIS and covers reading a source apart, building one from parts, handling database connection strings, and repointing a live layer.
Prerequisites
- QGIS 3.40 LTR or newer.
decodeUriexists from 3.10 andencodeUrifrom 3.12, so older installs work for everything except the newest provider keys. - A project or script with layers loaded from at least one file and, optionally, one database.
Read a layer's source apart
QgsProviderRegistry.decodeUri takes a provider key and a source string and returns a dictionary. The keys depend on the provider, so the first thing to do with an unfamiliar layer is print what comes back.
from qgis.core import QgsProject, QgsProviderRegistry
registry = QgsProviderRegistry.instance()
for layer in QgsProject.instance().mapLayers().values():
key = layer.providerType()
parts = registry.decodeUri(key, layer.source())
print(f"{layer.name():<24} {key:<14} {parts}")
Breakdown: providerType() returns the key the layer was created with — ogr for most vector files, gdal for rasters, postgres, delimitedtext, wms, memory and so on — and that key is what selects the right parser. For ogr the dictionary holds path, and where relevant layerName, layerId and subset; for gdal it holds path and sometimes layerName for multi-dataset containers; for postgres it holds nothing useful, because database URIs have their own class, covered below. A provider with no URI parser returns an empty dictionary rather than raising, so an empty result is a signal to fall back to the provider-specific class.
A practical use is an inventory: which files does this project actually depend on? Decoding every layer and collecting the path values gives a list you can check for existence, archive or copy alongside the project file.
import os
dependencies = {}
for layer in QgsProject.instance().mapLayers().values():
parts = registry.decodeUri(layer.providerType(), layer.source())
path = parts.get("path")
if path:
dependencies.setdefault(path, []).append(layer.name())
for path, names in sorted(dependencies.items()):
flag = "ok " if os.path.exists(path) else "MISSING"
print(flag, path, "->", ", ".join(names))
Breakdown: Grouping by path matters for GeoPackages, where five layers often share one file; the inventory then shows one dependency, not five. The existence check catches the most common reason a shared project opens with red warning triangles, and the same loop is the starting point for fixing broken layer paths automatically.
Build a source from parts
Going the other way, encodeUri assembles a source string. It is the safe way to add a layername to a container path or attach a subset filter, because the provider applies its own quoting rules.
from qgis.core import QgsVectorLayer
parts = {
"path": "/data/work/survey 2026.gpkg",
"layerName": "trees",
"subset": "\"height_m\" > 12",
}
uri = registry.encodeUri("ogr", parts)
print(uri)
tall_trees = QgsVectorLayer(uri, "tall trees", "ogr")
print(tall_trees.isValid(), tall_trees.featureCount())
Breakdown: The space in the file name and the quotes in the subset would each need escaping by hand; encodeUri produces a string the ogr provider reads back correctly. The subset key becomes the layer's subset string, so the filter survives saving the project. For a gdal raster inside a GeoPackage the same pattern works with layerName naming the raster table.
Database connection strings
Database providers use QgsDataSourceUri, which parses and builds the dbname='…' host=… table="schema"."table" (geom) format and exposes each part through a getter and a setter.
from qgis.core import QgsDataSourceUri
pg_layer = QgsProject.instance().mapLayersByName("parcels")[0]
uri = QgsDataSourceUri(pg_layer.source())
print(uri.host(), uri.database(), uri.schema(), uri.table(),
uri.geometryColumn(), uri.keyColumn(), uri.authConfigId())
uri.setConnection("db-new.internal", "5432", "gis", "", "",
QgsDataSourceUri.SslPrefer, uri.authConfigId())
uri.setDataSource("cadastre", "parcels_2026", "geom", "", "fid")
print(uri.uri(False))
Breakdown: setConnection takes host, port, database, user, password, SSL mode and an authentication configuration id; passing empty user and password with an authcfg keeps credentials out of the string entirely, which is the pattern QgsAuthManager is designed for. setDataSource takes schema, table, geometry column, an optional SQL filter and the key column. uri(False) returns the string without expanding the auth config, which is the form to store; uri(True) would inline the resolved credentials, which is the form never to print or save.
Web service parameters
WMS, WFS, ArcGIS REST and XYZ layers use a key=value&key=value form in which some values are themselves URLs. QgsDataSourceUri handles that form too, through setParam and param, and encodedUri() produces the escaped bytes the providers expect.
from qgis.core import QgsRasterLayer
wms = QgsDataSourceUri()
wms.setParam("url", "https://maps.example.org/wms?map=/srv/base.map")
wms.setParam("layers", "topo")
wms.setParam("styles", "")
wms.setParam("format", "image/png")
wms.setParam("crs", "EPSG:3857")
wms.setAuthConfigId("wms0001")
source = bytes(wms.encodedUri()).decode()
topo = QgsRasterLayer(source, "topo", "wms")
print(topo.isValid())
existing = QgsDataSourceUri()
existing.setEncodedUri(topo.source())
print(existing.param("url"), existing.param("layers"))
Breakdown: The service URL contains its own ? and =, which is exactly what breaks string concatenation; encodedUri() escapes it so the provider sees one url parameter rather than several. setParam can be called repeatedly with the same key for parameters that legitimately repeat — a WMS request for several layers and matching styles — and params(key) returns them all. Reading an existing layer goes the other way through setEncodedUri, after which each parameter is available by name, so a script can swap a server URL across every WMS layer in a project without touching the layer names or styles. The same pattern builds sources for WMS and WFS layers.
Repoint a live layer
setDataSource swaps what a layer reads while keeping the layer object — its id, its style, its joins, its place in the layer tree and every layout item that refers to it. That is what distinguishes it from removing a layer and adding a new one.
from qgis.core import QgsDataProvider
roads = QgsProject.instance().mapLayersByName("roads")[0]
parts = registry.decodeUri("ogr", roads.source())
parts["path"] = "/srv/gis/base/base.gpkg"
new_source = registry.encodeUri("ogr", parts)
options = QgsDataProvider.ProviderOptions()
options.transformContext = QgsProject.instance().transformContext()
roads.setDataSource(new_source, roads.name(), "ogr", options)
print(roads.isValid(), roads.source())
roads.triggerRepaint()
Breakdown: Passing the project's transform context in the provider options keeps any datum transformation choices the project already made. The renderer is kept as long as the new source has a compatible geometry type and the fields the renderer references; point a categorised polygon layer at a table without the category field and the style survives but classifies nothing. isValid() afterwards is the check that the new source actually opened — setDataSource does not raise on failure.
QGIS version compatibility
decodeUri has been available since QGIS 3.10 and encodeUri since 3.12; both work identically on 3.40, 3.44 and the QGIS 4 series. setDataSource gained its ProviderOptions argument in 3.6 and a loadDefaultStyleFlag overload later; the four-argument form shown here is the portable one. On QGIS 4, QgsDataSourceUri.SslPrefer is spelled QgsDataSourceUri.SslMode.SslPrefer, which also works on recent 3.x releases.
Troubleshooting
decodeUrireturns an empty dictionary. The provider has no URI parser; for databases useQgsDataSourceUri, for web services read the parameters withQgsDataSourceUri.param().- The layer is invalid after
setDataSource. The provider key does not match the new source, or the file is not there. Checkroads.error().summary(). - Style is lost after repointing. The new source has a different geometry type, so QGIS reset the renderer.
- Credentials appear in a printed URI. You called
uri(True); printuri(False)instead. - A path with
|in the name is misread. It was split by hand. Decode it with the registry.
Conclusion
Treat source strings as serialised data, not text. Decode them with the registry, change only the key you mean to change, encode them again, and apply the result with setDataSource so that every style, join and layout reference stays attached to the layer.
Frequently Asked Questions
How do I find the file behind a layer in one line?QgsProviderRegistry.instance().decodeUri(layer.providerType(), layer.source()).get("path").
Can I convert a shapefile layer to point at a GeoPackage table?
Yes, provided the table has the same geometry type and the fields the style uses. Set path and layerName and call setDataSource with the ogr key.
Is there a list of every provider key?QgsProviderRegistry.instance().providerList() returns the keys available in your installation.
Why does source() sometimes differ from what I passed in?
Providers normalise what they are given. The ogr provider may append |layerid=0 to a single-layer file, and a project saved with relative paths stores ./data/roads.gpkg but hands source() back as an absolute path resolved against the project location. Decoding both strings and comparing the dictionaries is more reliable than comparing the raw text.
Can I tell whether two layers read the same table?
For file layers, compare the decoded path and layerName. For database layers, compare QgsDataSourceUri host, database, schema and table — two layers can share a table with different subset filters, so include sql() in the comparison if that distinction matters to your script.