Load a WMS Layer in PyQGIS
Adding a WMS layer through the QGIS interface is four clicks. Doing it from Python is one line — provided the URI is right, and getting it right is the entire task. The provider gives no encouragement when it is wrong: you get a layer object, isValid() returns False, and the error message is often a bare HTTP status. This recipe removes the guesswork by building the URI with the API instead of by hand, and by asking the service what it actually offers before requesting anything.
It belongs to Web Services and Remote Data in PyQGIS, which covers the wider question of when a rendered image service is the right choice at all.
Prerequisites
- QGIS 3.34 LTR (bundled Python 3.12) or newer.
- The base URL of a WMS service — the part before the question mark.
- Network access from the machine running the script, including through any proxy QGIS is configured to use.
Discover what the service publishes
Before hard-coding a layer name, ask the server. The provider metadata API parses the capabilities document for you.
from qgis.core import QgsProviderRegistry
metadata = QgsProviderRegistry.instance().providerMetadata("wms")
connection = metadata.createConnection("https://maps.example.org/geoserver/wms", {})
for layer in connection.tables():
print(layer.tableName())
Breakdown: createConnection() with a URL and an empty options dictionary builds a throwaway connection rather than saving one into the user's settings — appropriate in a script that only wants to look. tables() returns the advertised layers using the same interface the browser panel uses, so what you see here is exactly what a user would see. If this call fails, the problem is the service or the network, and no amount of URI tuning will help; that is useful to establish before spending an hour on parameters.
Build the URI and load the layer
from qgis.core import QgsDataSourceUri, QgsRasterLayer, QgsProject
uri = QgsDataSourceUri()
uri.setParam("url", "https://maps.example.org/geoserver/wms")
uri.setParam("layers", "topo:contours")
uri.setParam("styles", "")
uri.setParam("format", "image/png")
uri.setParam("crs", "EPSG:27700")
layer = QgsRasterLayer(str(uri.encodedUri(), "utf-8"), "Contours", "wms")
if not layer.isValid():
raise RuntimeError(f"WMS layer invalid: {layer.error().summary()}")
QgsProject.instance().addMapLayer(layer)
Breakdown: Five parameters are the minimum for a WMS source and each has a trap. url must be the endpoint without a query string — QGIS appends its own. layers is the machine name from the capabilities document, not the human title shown in the browser panel, and workspace-qualified names such as topo:contours need the colon escaped, which is precisely what setParam() does for you. styles must be present even when empty, and must have the same number of entries as layers when you request several. format has to be one the server advertises; image/png supports transparency, image/jpeg does not. crs must appear in that layer's advertised list — requesting one it does not offer is the single most common cause of a valid-looking but blank layer.
Requesting several layers in one source stacks them server-side, which is faster than adding them separately:
uri.setParam("layers", "topo:contours")
uri.setParam("layers", "topo:spot_heights")
uri.setParam("styles", "")
uri.setParam("styles", "")
Breakdown: setParam() called twice with the same key appends rather than replacing, which is how the WMS provider expresses a list. The styles list must be padded to match, empty strings included, or the server rejects the request with a mismatched-parameter error that names neither list.
Authentication and extra parameters
Services behind a login or an API key should not carry credentials in the URI, because the URI is written into the project file.
uri.setParam("authcfg", "a1b2c3d") # stored configuration id
uri.setParam("dpiMode", "7")
uri.setParam("contextualWMSLegend", "0")
Breakdown: authcfg refers to an entry in QGIS's encrypted authentication database, so the project can be shared without the secret travelling with it — the same mechanism used for database connections in Connect to a PostGIS Database in PyQGIS. dpiMode controls how QGIS reconciles its own resolution with the server's; 7 (all modes) is the tolerant default and is worth setting explicitly when a layout export comes out at the wrong scale. Any parameter the service supports but QGIS does not model — a time dimension, a vendor-specific option — can be appended to the url parameter as a query string, provided it is percent-encoded.
Query a WMS layer with GetFeatureInfo
A WMS layer has no attribute table, but many services answer a point query. The provider exposes it through the raster identify API.
from qgis.core import QgsRaster, QgsPointXY
result = layer.dataProvider().identify(
QgsPointXY(432100, 187650),
QgsRaster.IdentifyFormatHtml,
)
if result.isValid():
for key, value in result.results().items():
print(key, value)
Breakdown: identify() issues a GetFeatureInfo request at that map coordinate, in the layer's coordinate system, and returns whatever the server chooses to send — HTML for display, or IdentifyFormatFeature where the service returns structured features QGIS can turn into real feature objects. Support is optional and many services decline, so isValid() on the result is doing real work here. This is also the mechanism behind the identify tool in the interface, which makes it a good way to test a custom map tool of the kind built in Create a Custom Map Tool in PyQGIS.
Control transparency, scale limits and refresh
Once the layer loads, a few properties turn it from a technically-present base layer into one that behaves well in a real map.
basemap = layer # the WMS layer loaded above
basemap.renderer().setOpacity(0.65)
basemap.setScaleBasedVisibility(True)
basemap.setMinimumScale(250000) # hidden when zoomed out beyond this
basemap.setMaximumScale(1000) # hidden when zoomed in beyond this
basemap.triggerRepaint()
Breakdown: Opacity on the renderer rather than on individual pixels is the right lever for a background image, and 60 to 70 percent is the usual range that keeps an aerial photograph readable underneath vector work. Scale-based visibility is what stops a detailed service being requested at continental zoom, which is both slow and useless — note that in QGIS the minimum scale is the zoomed-out limit, which reads backwards until you remember that scale denominators get larger as you zoom out. triggerRepaint() applies the change immediately rather than waiting for the next pan.
Services whose content changes — a live radar composite, a daily satellite mosaic — need one more setting, or the user sees a cached image indefinitely:
basemap.setAutoRefreshInterval(300000) # milliseconds
basemap.setAutoRefreshEnabled(True)
Breakdown: Auto-refresh re-requests the layer on a timer, which is exactly right for an operational display and exactly wrong for a scheduled export, where it wastes requests on a map nobody is watching. Five minutes is a sensible floor for a public service; anything shorter should be against infrastructure you control. For a genuinely time-aware service, the WMS time dimension is passed as an extra query parameter on the url value, which lets you request a specific timestamp rather than whatever the server considers current.
QGIS version compatibility
| QGIS version | Python | Notes |
|---|---|---|
| 3.22 LTR | 3.9 | WMS 1.1.1 and 1.3.0, authcfg, and identify as described. |
| 3.28 LTR | 3.9 | Identical. |
| 3.34 LTR | 3.12 | Baseline for this page; provider connection API available for discovery. |
| 3.40 / 3.44 | 3.12 | Identical loading behaviour; the browser panel gained better capabilities caching, which does not change these calls. |
WMS version is negotiated by the provider. Where a server misbehaves on 1.3.0 — the axis-order change between versions is a classic source of maps that appear rotated or in the sea — pin it by adding version=1.1.1 to the url parameter's query string.
Troubleshooting
isValid()isFalsewith an HTTP 404. The endpoint URL is wrong, or a query string was left on it. Strip everything from the question mark onwards.- HTTP 401 or 403. The service needs credentials. Create an authentication configuration and pass
authcfg; check whether the key must be a header rather than a parameter. - The layer is valid but the canvas is empty. Either the requested CRS is not supported for that layer, or the canvas is outside the layer's advertised extent. Zoom to
layer.extent()first. - The image is rotated or in the wrong hemisphere. Axis-order confusion between WMS 1.1.1 and 1.3.0. Pin the version in the URL.
- Everything below the layer is hidden. JPEG has no alpha channel. Request
image/png, and set the layer's own transparency if the server bakes in a background. - It works interactively but not in a scheduled job. The job runs as a different user, without the proxy settings or the authentication database the interactive session has.
Conclusion
Build the URI with QgsDataSourceUri and setParam() so escaping is handled for you, take the layer name from the capabilities document rather than the title, request a CRS the layer actually supports, and check isValid() before adding the layer to the project. When something goes wrong, read the provider's error text first — it distinguishes an authentication problem from a naming problem from an unsupported projection, which are three very different afternoons.
Frequently Asked Questions
Can I run analysis on a WMS layer? No, in any meaningful sense: it delivers a picture, not values. Look for a WCS or WFS endpoint on the same service, or a downloadable dataset, when you need the numbers behind the image.
How do I add several WMS layers from one service?
Either add them as separate layers, or request them in one source by calling setParam("layers", ...) repeatedly with a matching number of styles entries. One source is faster; separate layers give the user individual visibility control.
Why does the WMS look worse in a layout than on screen?
The export requests a much larger image, and many servers cap the pixel dimensions of a single GetMap response. Set dpiMode explicitly, and consider tiling the export or using a vector source for print.
Where do I put an API key?
In a QGIS authentication configuration referenced by authcfg. Keys pasted into the URI are saved into the project file and travel with it.
Does a WMS layer work offline? Only from cache, and not reliably. Cache what you need locally before going into the field — the caching pattern is in Web Services and Remote Data in PyQGIS.