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.

Decode, change one part, encodeA layer's source string goes into decodeUri with the provider key and comes out as a dictionary with keys such as path and layerName. The script changes only the path. encodeUri turns the dictionary back into a correctly quoted source string, and setDataSource points the existing layer at it while keeping its style and id.Never edit the string — edit the partslayer.source()/old/share/base.gpkg|layername=roadsdecodeUri("ogr", …)path: /old/share/…layerName: roadschange one keypath: /new/gis/base.gpkglayerName: roadsencodeUri("ogr", parts)quoting handled for youlayer.setDataSource(…)same layer id, style, joinsworks for ogr, gdal,delimitedtext, wms …

Prerequisites

  • QGIS 3.40 LTR or newer. decodeUri exists from 3.10 and encodeUri from 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.

Three families of source stringFile providers such as ogr, gdal and delimitedtext use a path-based string handled by decodeUri and encodeUri. Database providers such as postgres, spatialite and oracle use key equals quoted value pairs handled by QgsDataSourceUri with connection setters and setDataSource. Web providers such as wms, wfs and arcgisfeatureserver use key value parameters handled by QgsDataSourceUri setParam and encodedUri.Use the parser that matches the provider familyfilesogr · gdal · delimitedtextdecodeUri / encodeUripath plus layerName,subset, layerIddatabasespostgres · spatialite · mssqlQgsDataSourceUrihost, db, schema, table,geometry column, keyweb serviceswms · WFS · arcgisfeatureserversetParam / encodedUriurl, layers, typename,crs, authcfg

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.

What survives a source swapTwo columns. setDataSource keeps the layer id, the renderer and labels, joins and relations, layer tree position, and references from layouts and map themes. Removing the layer and adding a new one produces a new id, default style, no joins, the bottom of the layer tree, and broken layout and theme references.Swap the source, keep the layersetDataSource✓ same layer id✓ renderer, labels, opacity✓ joins and relations✓ position in the layer tree✓ layout maps and map themesremove + add new layer✗ new id, old references dangle✗ default random symbol✗ joins and relations gone✗ lands at the top of the tree✗ themes forget it existed

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

  • decodeUri returns an empty dictionary. The provider has no URI parser; for databases use QgsDataSourceUri, for web services read the parameters with QgsDataSourceUri.param().
  • The layer is invalid after setDataSource. The provider key does not match the new source, or the file is not there. Check roads.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); print uri(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.