QGIS Python Version Compatibility Guide

QGIS ships with a strictly version-locked Python interpreter. The bundled Python minor version changes with each major release, and you cannot safely swap it for a system Python, a conda environment, or a standalone installer without breaking the compiled qgis.core and qgis.gui bindings. If you have ever seen ImportError: DLL load failed or ModuleNotFoundError: qgis right after installing a package, you have hit this wall. This guide explains why the lock exists, how to read the compatibility matrix, and the exact steps to install dependencies and recover from an ABI mismatch — always matching external packages to the precise minor version QGIS provides.

Rule of thumb: Never assume a Python wheel built for your operating system's system Python will work inside QGIS. Verify that the ABI tag (cp39, cp310, cp312) matches the QGIS runtime before installing any C-extension package.

The QGIS version-lock chainA QGIS release pins one bundled Python minor version, which pins one C-extension ABI tag such as cp312. A wheel tagged cp312 matches the runtime and imports cleanly, while a cp311 wheel built for a system Python breaks the chain with ImportError: DLL load failed.The QGIS version-lock chainOne release pins one Python minor, which pins one ABI tag, which decides which wheels loadQGIS releasee.g. 3.34 / 3.44 LTRBundled Python3.12.x, version-lockedC-extension ABIcp312pinspinsWhich wheel loads inside this runtime?Wheel tagged cp312matches Python 3.12✓ Loads — qgis.core importsWheel tagged cp311built for system Python 3.11✗ ImportError: DLL load failedPure-Python wheels carry no ABI tag and load across any minor version — only C-extensions are locked.

Prerequisites

Before working through the recipes below, make sure you have:

  • A working QGIS install — this guide covers the 3.16 through 4.0 range. If PyQGIS is not yet importable on your machine, start with How to Install QGIS Python Bindings on Windows.
  • Access to the QGIS Python Console (Plugins > Python Console), or the bundled shell launcher (python-qgis-ltr.bat on Windows).
  • A basic grasp of the binding layer — the QGIS API Architecture that generates qgis._core from C++ via SIP. That article explains the compiled layer this whole compatibility story rests on.
  • Knowing where your QGIS user profile lives, because ABI recovery involves clearing manually copied packages from it.

Why Version Locking Matters

The QGIS API relies on SIP-generated Python bindings compiled against a specific Python ABI and a specific Qt/PyQt build. The Application Binary Interface (ABI) is the low-level contract — struct layouts, symbol names, reference-counting behaviour — between compiled qgis._core objects and the CPython interpreter that loads them. When Python's minor version changes (say 3.9 to 3.12), that contract changes too, and the pre-compiled C-extensions inside qgis._core become binary-incompatible with the interpreter trying to import them.

That is why mixing an external Python installation with QGIS produces errors like ImportError: DLL load failed (Windows) or ImportError: cannot import name 'QgsApplication' (any OS). The Python source looks identical; the binary does not line up. Treat the bundled interpreter as an immovable part of the runtime rather than a component you can upgrade independently.

Verify Your Active Environment

The first concrete step is always to confirm which interpreter is actually running. Paste this into the QGIS Python Console to print the alignment between the interpreter, the QGIS release, and the on-disk binding module:

import sys
import qgis.core

print(f"QGIS Python: {sys.version_info.major}.{sys.version_info.minor}")
print(f"QGIS version: {qgis.core.Qgis.QGIS_VERSION}")
print(f"QGIS Core Path: {qgis.core.__file__}")

if sys.version_info < (3, 9):
    raise RuntimeError("Python < 3.9 is incompatible with QGIS 3.16+")

The minor version reported here — 3.9, 3.10, or 3.12 — is the number every C-extension you install must target. Note it down before touching pip.

Safe Dependency Installation

Once you know the runtime's Python version, add third-party libraries without breaking bindings by treating QGIS as a self-contained runtime, following the broader PyQGIS Fundamentals & Environment Setup workflow.

  1. Use the bundled pip. QGIS 3.22+ includes pip in the console. Always target sys.executable so the package lands in the QGIS interpreter's site-packages, not a system one:
    import subprocess, sys
    subprocess.check_call([sys.executable, "-m", "pip", "install", "requests"])
    
  2. Windows: Run external scripts via python-qgis.bat or python-qgis-ltr.bat (in the QGIS bin folder). This launcher sets PYTHONHOME, PYTHONPATH, and QT_PLUGIN_PATH automatically, so the correct interpreter and Qt plugins are resolved for you.
  3. Linux/macOS: Invoke scripts using the QGIS-bundled Python executable directly. Never manually export PYTHONPATH to system Python directories when targeting the QGIS runtime.

If you need a fully external process — for heavy data crunching that never imports qgis — drive it through a separate interpreter as described in Running Python Scripts Outside QGIS Desktop.

The release train and what it locks

Each QGIS release ships with one Python version and one Qt version, and those choices propagate to everything you can install alongside them.

What each QGIS release locks inThree long-term releases are shown in sequence. QGIS 3.28 ships Python 3.9 and Qt 5. QGIS 3.34 ships Python 3.12 and Qt 5. QGIS 3.40 ships Python 3.12 with a Qt 6 build appearing alongside. Each release keeps those versions for its whole lifetime, so any package installed beside it must be compatible with them.You do not choose the Python version — the release doesQGIS 3.28 LTRPython 3.9Qt 5older syntax onlyQGIS 3.34 LTRPython 3.12Qt 5the baseline hereQGIS 3.40 LTRPython 3.12Qt 5 · Qt 6 buildstwo Qt worldsoldernewer

Which Python you are actually running

Three interpreters commonly exist on one machine, and a script that works in the Console and fails in a terminal is nearly always running under a different one.

Three interpreters, one set of bindingsThe QGIS bundled interpreter can import the bindings directly. The system Python cannot unless its version matches and the path is configured. A virtual environment created from the bundled interpreter with system site packages enabled can, while one created from the system Python usually cannot.Check with: python3 -c "import sys; print(sys.executable)"QGIS bundled/usr/bin/python3imports the bindingsuse this for scriptssystem Python/usr/local/bin/python3?only if the version matchesand PYTHONPATH is seta virtualenv.venv/bin/pythonif built from the bundled onewith system site packages

QGIS-Version Compatibility Notes

Use this matrix to map any QGIS release to its bundled Python and the ABI tag your wheels must carry. Because the interpreter is shared across a release range, the table groups releases by the Python they ship.

QGIS Release RangeBundled PythonABI TagCompatibility Notes
3.16–3.223.9.xcp39Legacy LTR. Requires pip install --ignore-installed for reinstalls.
3.24–3.283.9.xcp39Stable series. C-extensions must target the 3.9 ABI.
3.30–3.323.10.xcp310Transition builds. macOS Homebrew may ship mismatched wheels.
3.34–3.363.12.xcp312Requires updated C-extensions. PyQt5 remains the default.
3.38–3.443.12.xcp312Includes the 3.40 and 3.44 LTRs — the final 3.x (Qt5/PyQt5) series.
4.0+3.12.x+cp312+Qt6/PyQt6 generation. Confirm with sys.version inside the QGIS Console; release notes may revise this.

Two rules follow directly from the table:

  • Pin your code to an LTR. The 3.34 and 3.44 LTRs both bundle Python 3.12, so examples pinned to cp312 remain valid across that whole span. Pure-Python packages with no compiled code are generally safe across minor versions; only C-extensions care about the ABI tag.
  • A minor-version bump is a breaking change. Moving from a 3.9 release to a 3.12 release invalidates every previously installed C-extension. Plan the recompile before you upgrade, not after.

Troubleshooting: Recover from ABI Mismatches

If you encounter ImportError: cannot import name 'QgsApplication', DLL load failed, or silent crashes after installing packages, work through these steps in order:

  1. Clear corrupted packages. Delete manually copied folders in the QGIS user profile:
    • Windows: %APPDATA%\QGIS\QGIS3\profiles\default\python\
    • Linux/macOS: ~/.local/share/QGIS/QGIS3/profiles/default/python/
  2. Force a reinstall via the bundled pip. From the Python Console:
    import subprocess, sys
    subprocess.check_call([sys.executable, "-m", "pip", "install", "--force-reinstall", "package_name"])
    
  3. Use OSGeo4W for geospatial wheels (Windows). For GDAL, Fiona, or Shapely, install via the OSGeo4W installer. It compiles binaries against the exact Python version and MSVC runtime QGIS expects, preventing C-extension segfaults that generic PyPI wheels can cause.
  4. Avoid venv for QGIS plugins. Copying qgis and PyQt5 into a virtual environment only works temporarily and breaks on the next update. Reserve external environments for pure data processing (pandas, rasterio) that runs outside the QGIS process.

Deployment checklist

  • Match your plugin's metadata.txt qgisMinimumVersion to the QGIS version you tested against.
  • Never hardcode sys.path. Use os.environ["PYTHONPATH"] only when spawning external processes.
  • Audit C-extensions before upgrading QGIS. A Python minor-version bump (e.g. 3.9 → 3.12) requires recompiling or sourcing new ABI-compatible wheels for every C-extension dependency.
  • Keep qgis.core, qgis.gui, and qgis.analysis imports strictly inside the bundled runtime.

Conclusion

QGIS compatibility comes down to a single chain: the QGIS release fixes the bundled Python minor version, that minor version fixes the ABI tag, and the ABI tag decides which wheels load. Confirm the interpreter with sys.version_info in the console, install every C-extension against the matching cp39/cp310/cp312 tag using the bundled pip, and never try to graft a system or conda Python onto the runtime. When you do upgrade across a minor-version boundary, treat it as a breaking change and recompile your compiled dependencies. Follow those rules and the version-lock stops being a source of cryptic import errors and becomes a predictable constraint you design around.

Frequently Asked Questions

Which Python version does QGIS 3.34 LTR ship with? QGIS 3.34 ships with Python 3.12.x, and that interpreter is shared across the whole 3.34–3.36 range. Any C-extension you install for use inside QGIS must therefore target the cp312 ABI. Run sys.version_info in the QGIS Python Console to confirm the exact minor version on your install.

Can I upgrade the Python interpreter that QGIS uses? No. The bundled Python is version-locked because the SIP-generated qgis._core and qgis._gui bindings are compiled against one specific Python ABI and Qt build. Swapping in a system Python, conda environment, or newer installer breaks those bindings and produces ImportError: DLL load failed or ModuleNotFoundError: qgis. Treat QGIS as a self-contained runtime instead.

How do I check whether a wheel is compatible with my QGIS install? Match the wheel's ABI tag to the QGIS Python minor version: cp39 for QGIS 3.16–3.28, cp310 for 3.30–3.32, and cp312 for 3.34 and later. A wheel built for your operating system's system Python will not necessarily match, so verify the tag before installing any C-extension package. Pure-Python packages with no compiled code are generally safe across minor versions.

Why do I get an ABI mismatch after upgrading QGIS? A QGIS upgrade that bumps the Python minor version (for example 3.9 to 3.12) invalidates every previously installed C-extension, because those binaries were compiled against the old ABI. Reinstall the affected packages with pip install --force-reinstall from the bundled pip, or source new ABI-compatible wheels. Pure data-processing libraries running outside the QGIS process in their own environment are unaffected.

Should I use a virtual environment for QGIS plugin dependencies? Not for code that imports qgis or PyQt5. Copying those bindings into a venv only works temporarily and breaks on the next QGIS update. Reserve virtual environments for pure data-processing dependencies such as pandas or rasterio that run outside the QGIS process, and let QGIS manage its own bundled packages.