Use PyQGIS with Conda and Mamba
The bundled QGIS Python is convenient and inflexible: one version, one set of packages, shared with the desktop application. A conda environment inverts that — QGIS becomes a package like any other, several versions can coexist, and the whole thing is reproducible from a text file. For anything that has to run on a build server, in a container or alongside a scientific stack, it is the arrangement that causes the least trouble.
This recipe belongs to Virtual Environments for GIS in PyQGIS. It covers creating a QGIS environment from conda-forge, pinning versions so the environment is reproducible, the activation details that differ per platform, and where the conda route is and is not appropriate.
Prerequisites
- A conda distribution. Miniforge is the one to install: it defaults to conda-forge, which is where the QGIS packages live, and it ships
mambaas the solver. - Roughly 3 GB of disk per environment. QGIS pulls in Qt, GDAL, PROJ, GEOS and a large dependency tree.
- On Linux, a working X or Wayland display only if you intend to open the GUI; headless use needs none.
Create the environment
One command, and the version pin is the important part of it.
mamba create -n qgis-334 -c conda-forge "qgis=3.34.*" python=3.12 -y
mamba activate qgis-334
python -c "import qgis.core; print(qgis.core.Qgis.QGIS_VERSION)"
Breakdown: Pinning to 3.34.* gets the LTR line and its patch releases rather than whatever is newest, which is what makes the environment stable over months. Pinning Python explicitly avoids the solver choosing a different interpreter on a later rebuild and silently changing behaviour. mamba is a drop-in replacement for conda with a much faster solver — on a dependency tree this size the difference is minutes. Verifying the import immediately is worth the extra line, because a solve that succeeded does not guarantee the Qt libraries load.
Note there is no need to set PYTHONPATH or QGIS_PREFIX_PATH by hand. The conda package installs the Python bindings into the environment's site-packages and sets the prefix through activation scripts, which is the main practical advantage over pointing a virtualenv at a system install as described in running Python scripts outside QGIS Desktop.
Initialise QGIS in a script
Inside the environment the standalone initialisation is the ordinary one.
import os
from qgis.core import QgsApplication
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
QgsApplication.setPrefixPath(os.environ["CONDA_PREFIX"], True)
app = QgsApplication([], False)
app.initQgis()
from qgis.analysis import QgsNativeAlgorithms
import processing
from processing.core.Processing import Processing
Processing.initialize()
QgsApplication.processingRegistry().addProvider(QgsNativeAlgorithms())
# ... work ...
app.exitQgis()
Breakdown: CONDA_PREFIX is set by activation and points at the environment root, which is exactly where the conda package puts QGIS's resources — so the prefix path needs no hard-coded location. QT_QPA_PLATFORM=offscreen is what lets the whole thing run on a machine with no display, which is the usual reason for choosing conda in the first place; without it, Qt aborts on a headless server with a message about not finding a display. Importing processing before Processing.initialize() is the required order, and the native algorithm provider must be added explicitly or processing.run finds nothing.
Make the environment reproducible
An environment created by hand is not reproducible. An environment file is.
name: qgis-334
channels:
- conda-forge
dependencies:
- python=3.12
- qgis=3.34.*
- gdal=3.8.*
- pytest=8.*
- pip
- pip:
- pytest-qgis
Breakdown: Listing gdal explicitly alongside qgis looks redundant — QGIS depends on it — but it pins the version the solver may otherwise move, and GDAL version changes are the most common cause of a workflow behaving differently between machines. The pip: section handles packages not on conda-forge; keeping it short matters, because mixing pip and conda in one environment is where dependency resolution stops being reliable. Recreating is then mamba env create -f environment.yml, and mamba env export --from-history > environment.yml regenerates the file from what was actually requested rather than from the full resolved tree.
For a genuinely locked environment, conda-lock produces a platform-specific lock file with exact builds — the difference between "the same requests" and "the same bytes", and worth the extra tool for anything running in CI.
A smoke test worth keeping
Environments break silently. A GDAL upgrade changes a driver, a Qt build lands without the offscreen plugin, a solve quietly moves PROJ and every transform shifts. A short script run at the end of every environment build catches all three in about two seconds.
import os
from qgis.core import (
QgsApplication, QgsVectorLayer, QgsCoordinateReferenceSystem,
QgsCoordinateTransform, QgsCoordinateTransformContext, QgsPointXY, Qgis,
)
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
QgsApplication.setPrefixPath(os.environ["CONDA_PREFIX"], True)
app = QgsApplication([], False)
app.initQgis()
print("QGIS ", Qgis.QGIS_VERSION)
memory = QgsVectorLayer("Point?crs=EPSG:4326", "smoke", "memory")
assert memory.isValid(), "memory provider unavailable"
src = QgsCoordinateReferenceSystem("EPSG:4326")
dst = QgsCoordinateReferenceSystem("EPSG:3857")
transform = QgsCoordinateTransform(src, dst, QgsCoordinateTransformContext())
point = transform.transform(QgsPointXY(-2.0, 52.0))
assert abs(point.x() + 222638.98) < 1.0, f"projection maths looks wrong: {point.x()}"
from processing.core.Processing import Processing
from qgis.analysis import QgsNativeAlgorithms
Processing.initialize()
QgsApplication.processingRegistry().addProvider(QgsNativeAlgorithms())
assert QgsApplication.processingRegistry().algorithmById("native:buffer"), "no native algorithms"
print("smoke test passed")
app.exitQgis()
Breakdown: Each assertion targets a different failure mode: the memory provider proves the core libraries loaded, the coordinate check proves PROJ found its data files, and the algorithm lookup proves Processing registered. Checking an actual projected coordinate rather than merely that the transform did not raise is what catches a PROJ installation missing its database — a broken transform frequently returns the input unchanged rather than failing. Running this as the last step of an environment build, and again in CI before the real tests, turns a mysterious afternoon into a two-line error message.
Platform quirks
Each platform has one thing that catches people out.
On Linux, a headless machine needs QT_QPA_PLATFORM=offscreen and, for some Qt builds, the xvfb fallback. Containers additionally need libgl1 present even for offscreen rendering.
On macOS, the conda QGIS is a genuinely separate installation from a .dmg install and does not share its plugins or profile. Apple Silicon builds are available on conda-forge but occasionally lag the Intel ones by a release.
On Windows, activation must happen through the conda shell hook — running the environment's python.exe directly without activating leaves the DLL search path unset, and imports fail with an opaque message about a missing module that is plainly present on disk. In a scheduled task, call conda activate in the batch file rather than pointing at the interpreter.
When not to use conda
Conda is the wrong choice for developing a plugin that will run in a user's desktop QGIS. The environment's QGIS is a different installation with a different profile, so a plugin developed against it can pass every test and still fail on the machine it is meant for. Develop plugins against the same install that will run them, and see setting up PyCharm for QGIS for that arrangement.
It is also unnecessary overhead when a script only ever runs on one machine that already has QGIS installed and does not need extra packages. The conda route earns its keep when reproducibility, multiple versions or a headless server are involved — which is to say, most automation and almost no interactive work.
QGIS version compatibility
The examples target QGIS 3.34 LTR (Python 3.12).
| QGIS version | Python | Notes |
|---|---|---|
| 3.22 LTR | 3.9 | Available on conda-forge; older builds may be unavailable for Apple Silicon. |
| 3.28 LTR | 3.9 | Widely used conda-forge build; stable dependency tree. |
| 3.34 LTR | 3.12 | Baseline for this page. |
| 3.40 | 3.12 | Newer GDAL and PROJ; pin GDAL explicitly if a workflow depends on its behaviour. |
| 3.44+ | 3.12 | Qt6 builds appearing; check that any PyQt code targets the right binding. |
Troubleshooting
ImportError: DLL load failedon Windows. The environment was not activated. Use the conda shell hook rather than callingpython.exedirectly.- Qt aborts about a display on a server. Set
QT_QPA_PLATFORM=offscreenbefore importing. processing.runcannot find any algorithm.Processing.initialize()was skipped, or the native provider was never added.- The solve takes forever or fails. Use
mamba, and make sure conda-forge is the only channel — mixing defaults with conda-forge is the usual cause. - GDAL behaves differently from the desktop. The environment has a different GDAL version. Pin it explicitly.
- A plugin works in conda and not in the desktop. They are separate installations with separate profiles. Test against the target install.
Conclusion
Use Miniforge, create the environment with a pinned QGIS and Python, set QT_QPA_PLATFORM=offscreen for headless work, and record the environment in a file rather than in your shell history. Reach for conda when reproducibility or multiple versions matter, and stay with the system install when developing something that has to run inside somebody's desktop QGIS.
Frequently Asked Questions
Can I run QGIS Desktop from a conda environment?
Yes — qgis on the command line inside the environment launches the full application, with its own profile directory. It is a useful way to test a workflow against a different version without a second system install.
Does conda give me the same plugins? No. The environment has its own profile, so plugins must be installed into it separately. That isolation is the point, and it is also why it is unsuitable for plugin development.
Can I pip install packages alongside it? Yes, but conservatively. Anything with compiled dependencies — especially anything linking GDAL or GEOS — should come from conda-forge, or the two copies of the library will conflict. See installing Python packages into QGIS for the equivalent discussion on a system install.
Is this how I should set up CI? It is the most common approach, and pairs well with a lock file. See running QGIS plugin tests in GitHub Actions for the container-based alternative.