Enable PyQGIS Autocompletion with Type Stubs
The QGIS Python API is generated from C++ through SIP, and the result is a set of extension modules an editor cannot introspect. Without help, QgsVectorLayer. offers nothing, every method call is untyped, and a misspelt attribute is discovered at runtime. Stub files fix that: plain-text declarations of every class and signature that the language server reads instead of trying to inspect a binary.
This recipe belongs to Setting Up PyCharm for QGIS. It covers what QGIS already ships, installing stubs for the parts it does not, configuring Pylance and PyCharm to find them, and the limits of what stubs can tell you about a dynamically typed API.
Prerequisites
- QGIS 3.34 LTR (bundled Python 3.12) or newer.
- An editor with a language server: VS Code with Pylance, or PyCharm. Both read
.pyifiles; neither finds them automatically for QGIS. - A place to install packages that your editor's interpreter can see — the conda environment or virtualenv you point the editor at.
What QGIS already ships
More than most people realise. Recent QGIS builds include generated .pyi files alongside the compiled modules.
import qgis.core, os, glob
directory = os.path.dirname(qgis.core.__file__)
print(directory)
print([os.path.basename(p) for p in glob.glob(os.path.join(directory, "*.pyi"))])
Breakdown: If this prints _core.pyi and friends, the declarations are already on disk and the only remaining problem is that the editor is not looking there — which the analysis-path settings below solve. If it prints an empty list, the build did not ship them and third-party stubs are the route. The check takes ten seconds and determines which half of this page applies.
The shipped stubs cover qgis.core, qgis.gui, qgis.analysis and qgis.server. They do not cover processing, which is ordinary Python and needs no stub, nor PyQt, which needs its own.
Install stubs for what is missing
Two packages cover the gaps.
python -m pip install PyQt5-stubs
python -m pip install qgis-stubs
Breakdown: PyQt5-stubs is the widely used community package for the Qt bindings, and it is what makes QColor, QWidget and the signal machinery complete properly — a large share of plugin code is Qt rather than QGIS. qgis-stubs supplies QGIS declarations for builds that ship none; installing it alongside shipped stubs is harmless but redundant, and on a mismatched version it is worse than redundant because the signatures may not match the QGIS actually installed. Check the shipped stubs first.
Install into the environment the editor uses. Installing into the QGIS system Python from a terminal is a common mis-step that changes nothing about completion, because the editor was pointed at a different interpreter.
Configure Pylance
Pylance needs to be told where to look and how strictly to check.
{
"python.analysis.extraPaths": [
"/usr/share/qgis/python",
"/usr/share/qgis/python/plugins"
],
"python.analysis.stubPath": "${workspaceFolder}/typings",
"python.analysis.typeCheckingMode": "basic",
"python.analysis.useLibraryCodeForTypes": true
}
Breakdown: stubPath points at a local directory for stubs you write yourself, which is how a plugin's own generated resources module gets typed. useLibraryCodeForTypes: true lets Pylance fall back to inspecting installed packages where no stub exists — worth having on, because it makes processing complete. typeCheckingMode: "strict" is tempting and usually counterproductive on QGIS code: the API returns bare object in enough places that strict mode produces more noise than signal. Start at basic and raise it for your own modules with a per-directory override if you want.
Configure PyCharm
PyCharm handles this through the interpreter's path configuration rather than through a settings file.
Open Settings → Project → Python Interpreter, click the gear, choose Show All, select the interpreter, then the Show paths icon. Add the QGIS python and python/plugins directories. PyCharm indexes them, finds the .pyi files, and completion works from the next reindex.
The one non-obvious detail is that PyCharm prefers a stub to the real module when both are present, so a stale qgis-stubs install against a newer QGIS produces confidently wrong completion — offering methods that no longer exist and hiding ones that do. When completion disagrees with the running code, uninstalling the third-party stubs is the first thing to try.
Write a stub for your own generated code
A plugin compiled from a .qrc produces resources.py, which is machine-generated, enormous and useless to complete against. A three-line stub replaces it.
# typings/resources.pyi
def qInitResources() -> None: ...
def qCleanupResources() -> None: ...
Breakdown: Putting this in the directory named by stubPath makes the editor treat those two functions as the module's entire public surface, which is true. It removes several thousand lines of base64 from the index and makes the import resolve cleanly. The same trick applies to any generated module — a compiled .ui conversion, a vendored library with no annotations — and costs a minute each.
Annotating your own code so the stubs pay off
Stubs give the editor knowledge about the QGIS API. Annotating your own functions is what lets that knowledge propagate through a codebase, and it is where most of the practical benefit appears.
from typing import Optional, Iterable
from qgis.core import QgsVectorLayer, QgsProject, QgsFeature
def layer_by_name(name: str) -> Optional[QgsVectorLayer]:
matches = QgsProject.instance().mapLayersByName(name)
if not matches:
return None
layer = matches[0]
return layer if isinstance(layer, QgsVectorLayer) else None
def selected_or_all(layer: QgsVectorLayer) -> Iterable[QgsFeature]:
if layer.selectedFeatureCount():
return layer.selectedFeatures()
return layer.getFeatures()
Breakdown: mapLayersByName() is declared as returning a list of QgsMapLayer, so the isinstance narrowing is what tells the editor — and the reader — that a vector layer is what comes back. Returning Optional rather than raising forces callers to handle the missing case, and the editor will flag one that does not. Iterable[QgsFeature] covers both branches, which return different concrete types; declaring the common interface is more honest than picking one.
Two annotations do disproportionate work in PyQGIS code. Narrowing a QgsMapLayer to its real subclass, as above, unlocks completion for everything downstream. And annotating a parameter as QgsVectorLayer rather than leaving it bare means every layer. inside the function completes — which, in a module of twenty helpers, is the difference between the stubs being useful and being theoretical.
What stubs cannot do
Stubs describe signatures, not behaviour, and three QGIS patterns defeat them.
Methods returning QgsMapLayer when the caller knows it is a QgsVectorLayer require a cast or an assertion for the editor to follow — layer = cast(QgsVectorLayer, project.mapLayersByName("x")[0]) is the idiom. Property dictionaries such as QgsFillSymbol.createSimple({...}) take arbitrary string keys, so no stub can validate them. And processing.run() returns a plain dict, so the output key is a string the editor cannot check — which is exactly the string most likely to be wrong, and the reason to compare it against outputDefinitions() as described in running a graphical model from Python.
QGIS version compatibility
The examples target QGIS 3.34 LTR (Python 3.12).
| QGIS version | Python | Notes |
|---|---|---|
| 3.16 LTR | 3.7 | Few or no shipped .pyi; third-party stubs are the main route. |
| 3.22 LTR | 3.9 | Generated stubs begin appearing in some distribution builds. |
| 3.28 LTR | 3.9 | Shipped stubs more complete across core, gui and analysis. |
| 3.34 LTR | 3.12 | Baseline for this page; shipped stubs cover most of the API. |
| 3.40+ | 3.12 | Qt6 builds need PyQt6 stubs rather than PyQt5-stubs. |
Troubleshooting
- Completion is empty despite installing stubs. They went into a different interpreter than the editor uses. Check which interpreter is selected.
- Completion offers methods that do not exist. A stale third-party stub is taking precedence. Uninstall it and rely on the shipped one.
import processingis flagged. Thepython/pluginsdirectory is missing from the analysis paths.- Every Qt call is untyped.
PyQt5-stubsis not installed, or the build is Qt6 and needs the PyQt6 equivalent. - Strict mode floods the problems panel. The API returns loosely typed values in many places. Use
basic. - PyCharm still shows nothing after adding paths. It needs a reindex — File → Invalidate Caches and restart.
Conclusion
Check what QGIS already ships before installing anything, add the python and python/plugins directories to the editor's analysis paths, install PyQt5-stubs for the Qt half, and keep type checking at basic. When completion and runtime disagree, suspect a stale stub package rather than the bindings.
Frequently Asked Questions
Do stubs change what runs?
No. A .pyi is never imported at runtime; it exists purely for static analysis. Deleting every stub changes nothing about behaviour.
Can I generate stubs myself?
Yes — stubgen from mypy produces a starting point from the compiled modules, though the result needs manual work for overloads and enums. It is a reasonable option for a build with no shipped stubs and no matching package.
Do stubs help the QGIS Python console? The built-in console does its own introspection and is unaffected. Completion there comes from the live objects — see running and saving scripts in the QGIS Python editor.
Should I commit a typings directory? Yes, for stubs you wrote about your own generated code. Not for third-party packages, which belong in the dependency file.