How to Fix "ModuleNotFoundError: No module named 'qgis'"
On this page
This is the first error almost everyone meets when they try to script QGIS from outside the application, and it is misleading in a specific way: it looks like a missing package, so people try to install one. There is no package to install. The QGIS Python bindings are compiled against the exact Qt, GDAL, and PROJ that QGIS was built with, and they ship inside QGIS itself. The fix is always the same shape β point Python at the bindings that are already on the machine, or run a Python that already has them.
Problem statement
You run a script and get one of these:
ModuleNotFoundError: No module named 'qgis'
ModuleNotFoundError: No module named 'processing'
Or you get past the first import and hit its close relatives, which have the same root cause:
ModuleNotFoundError: No module named 'PyQt6'
ImportError: libqgis_core.so.4.2.2: cannot open shared object file: No such file or directory
The situations are predictable: you opened a terminal and ran python3 script.py; you set up a virtualenv for the project; the code works in the QGIS Python Console but not outside it; it works for you but not under cron; or it worked until you upgraded QGIS. The Linux cases below were all run: QGIS 4.2 from the qgis.org Ubuntu repository, QGIS 3.44 in the official qgis/qgis:ltr Docker image, and QGIS 3.44 from conda-forge. The Windows and macOS commands come from those installers' layouts and were not run for this page.
Quick answer
pip install qgis fails β there is no package of that name on PyPI β and nothing you can install from there is the QGIS bindings. Do one of these instead:
Linux β install QGIS from your distribution or qgis.org, then check whether the system Python can already see it. Depending on the packaging, it can or it cannot:
python3 -c "import qgis.core; print(qgis.core.Qgis.QGIS_VERSION)"
# if that fails, the bindings are under /usr/share/qgis/python:
PYTHONPATH=/usr/share/qgis/python python3 -c "import qgis.core; print(qgis.core.Qgis.QGIS_VERSION)"
Windows β open the OSGeo4W Shell and use its Python launcher:
python-qgis-ltr.bat -c "import qgis.core; print(qgis.core.Qgis.QGIS_VERSION)"
macOS β use the interpreter inside the app bundle:
/Applications/QGIS.app/Contents/MacOS/bin/python3 -c "import qgis.core; print(qgis.core.Qgis.QGIS_VERSION)"
Any platform, cleanest for automation β a conda environment that contains QGIS, activated:
conda create -n qgis -c conda-forge qgis
conda activate qgis
python -c "import qgis.core; print(qgis.core.Qgis.QGIS_VERSION)"
Which Python, where the bindings are, how to reach them
Find out which Python you are actually running
Before anything else, confirm the interpreter. This is the answer more often than anything else on this page.
import sys
print(sys.executable)
print(sys.version.split()[0])
for p in sys.path:
if not p.startswith(str(Path.cwd())) and not p.endswith("/tools"): # skip this page's runner
print(" ", tidy(p))
/usr/bin/python3
3.14.4
/usr/share/qgis/python
/usr/share/qgis/python/plugins
/usr/lib/python314.zip
/usr/lib/python3.14
/usr/lib/python3.14/lib-dynload
~/.local/lib/python3.14/site-packages
/usr/local/lib/python3.14/dist-packages
/usr/lib/python3/dist-packages
/usr/lib/python3.14/dist-packages
Run the same lines in the QGIS Python Console (Plugins β Python Console). Compare the two sys.executable values and the two sys.path lists. The console's path contains the QGIS Python directories β the run above had /usr/share/qgis/python because this page sets it β and a fresh terminal's does not. That difference is the bug.
A virtualenv is the usual next culprit. python3 -m venv .venv creates an environment isolated from system site-packages by design, so a system-installed qgis module is deliberately hidden. --system-site-packages lets the system packages through β which helps only when the bindings are a system package:
for name, interpreter, extra in [
("system python3", "/usr/bin/python3", {}),
("system python3 + PYTHONPATH", "/usr/bin/python3", QGIS_PATH),
("venv", "venvs/plain/bin/python", {}),
("venv + PYTHONPATH", "venvs/plain/bin/python", QGIS_PATH),
("venv --system-site-packages", "venvs/syssite/bin/python", {}),
("venv --system-site-packages + PYTHONPATH", "venvs/syssite/bin/python", QGIS_PATH),
]:
print(f"{name:42} {try_import(interpreter, extra)}")
system python3 ModuleNotFoundError: No module named 'qgis'
system python3 + PYTHONPATH ok, QGIS 4.2.2-BelΓ©m do ParΓ‘
venv ModuleNotFoundError: No module named 'qgis'
venv + PYTHONPATH ModuleNotFoundError: No module named 'PyQt6'
venv --system-site-packages ModuleNotFoundError: No module named 'qgis'
venv --system-site-packages + PYTHONPATH ok, QGIS 4.2.2-BelΓ©m do ParΓ‘
Two lessons in six lines. On this QGIS 4.2 install the bindings live in /usr/share/qgis/python, which is on nobody's path β not even the system interpreter's β so --system-site-packages alone changed nothing. And a plain venv with the path set got past qgis and failed on PyQt6: the bindings need the Qt bindings too, which are system packages. Only the --system-site-packages venv with the path set could import both.
Check whether the bindings exist on disk
for label, command in [
("QGIS 4.2, qgis.org .deb", ["bash", "-c", "ls -d /usr/lib/python3/dist-packages/qgis /usr/share/qgis/python/qgis 2>/dev/null"]),
("QGIS 3.44, qgis/qgis:ltr image", ["docker", "run", "--rm", "qgis/qgis:ltr", "bash", "-c",
"ls -d /usr/lib/python3/dist-packages/qgis /usr/share/qgis/python/qgis 2>/dev/null"]),
("QGIS 3.44, conda-forge", ["bash", "-c", f"ls -d {CONDA_ENV}/share/qgis/python/qgis {CONDA_ENV}/lib/python3*/site-packages/qgis 2>/dev/null"]),
]:
found = subprocess.run(command, capture_output=True, text=True).stdout.split()
print(f"{label:32} {[tidy(p) for p in found]}")
QGIS 4.2, qgis.org .deb ['/usr/share/qgis/python/qgis']
QGIS 3.44, qgis/qgis:ltr image ['/usr/lib/python3/dist-packages/qgis']
QGIS 3.44, conda-forge ['./conda-qgis/share/qgis/python/qgis']
Three installs, three answers. The official Docker image puts the bindings in dist-packages, where any system Python finds them. The qgis.org 4.2 packages put them in /usr/share/qgis/python, which needs PYTHONPATH. conda-forge puts them in the environment's share/qgis/python and relies on its activation script to add that to PYTHONPATH.
If the directory exists, this is a path problem and the next section fixes it. If it does not, QGIS's Python support is genuinely not installed β on Debian and Ubuntu that means the python3-qgis package, and on Windows it means re-running the installer with the Python components.
Point Python at the bindings
On Linux, when the bindings are under /usr/share/qgis/python, add them β and the plugins directory, because Processing is a plugin:
export PYTHONPATH="/usr/share/qgis/python:/usr/share/qgis/python/plugins:${PYTHONPATH:-}"
python3 script.py
for label, extra in [("core path only", QGIS_PATH), ("core + plugins", QGIS_AND_PLUGINS)]:
print(f"{label:16} {try_import('/usr/bin/python3', extra, 'import qgis.core, processing')}")
core path only ModuleNotFoundError: No module named 'processing'
core + plugins ok, QGIS 4.2.2-BelΓ©m do ParΓ‘
Setting this in a wrapper script rather than inside Python is deliberate. sys.path.append fixes the Python import for that one process, but it cannot reach the other half of the problem: the dynamic linker, which finds libqgis_core.so from its own search path, fixed when the process starts. On the three Linux installs tested here the libraries sat in standard locations (/usr/lib, or the conda environment's lib) and nothing needed LD_LIBRARY_PATH; a QGIS built into /opt or unpacked from an archive does, and it has to be set before Python starts.
On Windows, do not try to reproduce the environment by hand. QGIS ships batch files that set every variable:
REM From the OSGeo4W Shell
python-qgis-ltr.bat script.py
Look in the QGIS installation's bin folder for python-qgis.bat or python-qgis-ltr.bat. Calling one of these from a scheduled task is the supported route; hand-assembling PATH, PYTHONPATH, PYTHONHOME, GDAL_DATA, and PROJ_LIB is a long afternoon that the batch file already spent.
Install a Python that has the bindings, instead
For automation, the most reliable answer is not to patch an environment but to create one that is correct by construction. conda-forge is the cross-platform way:
direct = try_import(f"{CONDA_ENV}/bin/python", {})
activated = subprocess.run([CONDA, "run", "-p", CONDA_ENV, "python", "-c",
"import qgis.core, sys; print(qgis.core.Qgis.QGIS_VERSION, 'on Python', sys.version.split()[0])"],
capture_output=True, text=True, env=dict(os.environ, QT_QPA_PLATFORM="offscreen"))
print("env's python, not activated:", direct)
print("conda run (activated): ", activated.stdout.strip() or activated.stderr.strip().splitlines()[-1])
print("environment size on disk: ", subprocess.run(["du", "-sh", CONDA_ENV], capture_output=True, text=True).stdout.split()[0])
env's python, not activated: ModuleNotFoundError: No module named 'qgis'
conda run (activated): 4.2.3-BelΓ©m do ParΓ‘ on Python 3.14.8
environment size on disk: 3.1G
That gives you QGIS, its bindings and matched GDAL and PROJ in one prefix, reproducible from an environment.yml. Two things the run showed. The environment's Python cannot import qgis until the environment is activated, because it is the activation script that adds share/qgis/python to PYTHONPATH β so a cron job must use conda run -p <env>, not <env>/bin/python. And conda-forge was one release series behind: in September 2026 it resolved to QGIS 3.44, while the qgis.org packages were on 4.2. For containers, the qgis/qgis image is the equivalent β pin a tag rather than using latest.
Verify the whole stack, not just the import
An import that succeeds is not the same as a working QGIS. Check that the resources are found too:
check = '''
import os
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from qgis.core import QgsApplication, Qgis, QgsCoordinateReferenceSystem
app = QgsApplication([], False)
app.initQgis()
print("QGIS :", Qgis.QGIS_VERSION)
print("prefix :", QgsApplication.prefixPath())
print("srs db :", QgsApplication.srsDatabaseFilePath())
print("CRS ok :", QgsCoordinateReferenceSystem("EPSG:27700").isValid())
app.exitQgis()
'''
print(subprocess.run(["/usr/bin/python3", "-c", check], capture_output=True, text=True,
env=dict(os.environ, **QGIS_PATH)).stdout)
QGIS : 4.2.2-BelΓ©m do ParΓ‘
prefix : /usr
srs db : /usr/share/qgis/resources/srs.db
CRS ok : True
If the import works but the CRS is not valid, QGIS cannot find its CRS database β a different problem with a similar-looking symptom, covered in running PyQGIS headless. On this install the prefix was found without setPrefixPath.
Diagnosing three environments
The environments used on this page
The page runs under the system Python with /usr/share/qgis/python on its path. The setup builds the two virtualenvs compared above and a conda-forge environment with QGIS (about 3 GB), and pulls nothing else: no data is needed to diagnose an import.
import os
import subprocess
import sys
from pathlib import Path
HOME = str(Path.home())
CONDA = os.environ.get("CONDA_EXE", f"{HOME}/miniconda3/condabin/conda")
CONDA_ENV = str(Path("conda-qgis").absolute())
QGIS_PATH = {"PYTHONPATH": "/usr/share/qgis/python"}
QGIS_AND_PLUGINS = {"PYTHONPATH": "/usr/share/qgis/python:/usr/share/qgis/python/plugins"}
for name, flags in (("plain", []), ("syssite", ["--system-site-packages"])):
if not Path("venvs", name).exists():
subprocess.run(["/usr/bin/python3", "-m", "venv", *flags, f"venvs/{name}"], check=True)
if not Path(CONDA_ENV).exists():
subprocess.run([CONDA, "create", "-q", "-y", "-p", CONDA_ENV, "-c", "conda-forge", "--override-channels", "qgis"],
check=True, capture_output=True)
def tidy(text):
return str(text).replace(str(Path.cwd()), ".").replace(HOME, "~")
def try_import(interpreter, extra_env, statement="import qgis.core"):
"""Run an import in a clean child process and report the outcome in one line."""
env = {"PATH": "/usr/bin:/bin", "HOME": HOME, "QT_QPA_PLATFORM": "offscreen", **extra_env}
run = subprocess.run([interpreter, "-c", f"{statement}; from qgis.core import Qgis; print(Qgis.QGIS_VERSION)"],
capture_output=True, text=True, env=env)
if run.returncode == 0:
return "ok, QGIS " + run.stdout.strip().splitlines()[-1]
return tidy(run.stderr.strip().splitlines()[-1])
print("page interpreter: Python", sys.version.split()[0], "|", try_import("/usr/bin/python3", QGIS_PATH))
page interpreter: Python 3.14.4 | ok, QGIS 4.2.2-BelΓ©m do ParΓ‘
Example 1: A diagnostic script to run when it breaks
Path("diagnose_qgis.py").write_text('''#!/usr/bin/env python3
"""Print everything needed to diagnose a failed QGIS import."""
import os
import sys
from pathlib import Path
home = str(Path.home())
exe = Path(sys.executable) # relative when it lives in this project
print("executable :", exe.relative_to(Path.cwd()) if exe.is_relative_to(Path.cwd()) else str(exe).replace(home, "~"))
print("version :", sys.version.split()[0])
print("venv :", sys.prefix != sys.base_prefix)
for var in ("PYTHONPATH", "QGIS_PREFIX_PATH", "LD_LIBRARY_PATH", "CONDA_PREFIX"):
print(f"{var:<17}:", os.environ.get(var, "<unset>").replace(home, "~"))
CANDIDATES = [
"/usr/lib/python3/dist-packages/qgis",
"/usr/share/qgis/python/qgis",
"/Applications/QGIS.app/Contents/Resources/python/qgis",
str(Path(os.environ.get("CONDA_PREFIX", "/nonexistent")) / "share/qgis/python/qgis"),
]
print("bindings on disk:")
for c in CANDIDATES:
print((" yes " if Path(c).is_dir() else " no ") + c.replace(home, "~"))
try:
import qgis.core
print("import : ok,", qgis.core.Qgis.QGIS_VERSION)
except Exception as exc:
print(f"import : {type(exc).__name__}: {exc}")
''')
run = subprocess.run(["venvs/plain/bin/python", "diagnose_qgis.py"], capture_output=True, text=True,
env={"PATH": "/usr/bin:/bin", "HOME": HOME})
print(tidy(run.stdout))
executable : venvs/plain/bin/python
version : 3.14.4
venv : True
PYTHONPATH : <unset>
QGIS_PREFIX_PATH : <unset>
LD_LIBRARY_PATH : <unset>
CONDA_PREFIX : <unset>
bindings on disk:
no /usr/lib/python3/dist-packages/qgis
yes /usr/share/qgis/python/qgis
no /Applications/QGIS.app/Contents/Resources/python/qgis
no /nonexistent/share/qgis/python/qgis
import : ModuleNotFoundError: No module named 'qgis'
A "yes" in the "bindings on disk" section plus a failed import means a path problem. No "yes" at all means an installation problem. That distinction is the whole diagnosis.
Example 2: A wrapper script that sets the environment, tested the way cron runs it
Path("run.sh").write_text('''#!/usr/bin/env bash
# run.sh - run a PyQGIS script with a correct environment
set -euo pipefail
export PYTHONPATH="/usr/share/qgis/python:/usr/share/qgis/python/plugins:${PYTHONPATH:-}"
export QT_QPA_PLATFORM=offscreen
exec /usr/bin/python3 "$@"
''')
Path("run.sh").chmod(0o755)
Path("job.py").write_text("import processing\nfrom qgis.core import Qgis\nprint('job ran on QGIS', Qgis.QGIS_VERSION)\n")
for label, command in [("python3 job.py", ["/usr/bin/python3", "job.py"]), ("./run.sh job.py", ["./run.sh", "job.py"])]:
run = subprocess.run(["env", "-i", f"HOME={HOME}", "PATH=/usr/bin:/bin", *command], capture_output=True, text=True)
print(f"{label:16} under env -i: {(run.stdout if run.returncode == 0 else run.stderr).strip().splitlines()[-1]}")
python3 job.py under env -i: ModuleNotFoundError: No module named 'processing'
./run.sh job.py under env -i: job ran on QGIS 4.2.2-BelΓ©m do ParΓ‘
env -i starts the command with an empty environment apart from HOME and a minimal PATH β close to what cron provides. The bare script failed; the wrapper, which exports everything it needs, worked.
Example 3: Failing early with a useful message
A script that dies on line one with ModuleNotFoundError tells the next person nothing. Say what is wrong.
import sys
try:
from qgis.core import QgsApplication, Qgis
except ImportError as exc:
sys.exit(
f"Cannot import the QGIS bindings ({exc}).\n"
f"Interpreter: {sys.executable}\n"
"This script must run with a Python that can see the QGIS bindings:\n"
" Linux : /usr/bin/python3, with PYTHONPATH=/usr/share/qgis/python if needed\n"
" Windows : python-qgis-ltr.bat from the OSGeo4W Shell\n"
" macOS : /Applications/QGIS.app/Contents/MacOS/bin/python3\n"
" conda : conda run -p <env with conda-forge::qgis> python script.py\n"
)
Example 4: An environment.yml for reproducible automation
name: gis
channels: [conda-forge]
dependencies:
- qgis=3.44 # pin the release series
- geopandas
- pyyaml
- pytest
conda env create -f environment.yml
conda run -n gis python -c "import qgis.core, geopandas; print('ok')"
Pinning the QGIS version matters more than it looks: 13 Processing algorithms changed their ids between 3.44 and 4.2 (algorithm not found has the list), so an unpinned environment can break a working job on an unrelated day. This is the same discipline as any other reproducible workflow.
Example 5: Making it work in Jupyter
The kernel is a Python interpreter like any other, so it needs to be one that can import qgis β and for a conda environment, one started with the environment's variables:
conda activate gis
conda install -c conda-forge ipykernel
python -m ipykernel install --user --name gis --display-name "Python (QGIS)"
Then pick Python (QGIS) as the notebook kernel. Start the QgsApplication once, in the first cell, and leave it running for the session β constructing a second one in the same process is not supported.
Why there is no pip package
The reason there is no pip package is worth understanding, because it stops the search for one. The qgis module is a thin Python layer over SIP-generated bindings to QGIS's C++ libraries β _core.so, _gui.so and friends in the qgis directory. Those bindings are compiled, and they link against a specific Qt, a specific GDAL, a specific PROJ, specific QGIS shared libraries and a specific Python. A wheel would have to bundle all of that or match your system exactly β which is precisely the problem conda and system packages exist to solve. (What PyPI does have is qgis-stubs: type hints for editors, with no bindings in it.)
That framing turns the error into a question with three possible answers. Wrong interpreter or path: the bindings are on the machine and your Python cannot see them β fix with PYTHONPATH, a --system-site-packages virtualenv where the bindings are a system package, or an activated conda environment. Not installed: QGIS is present but its Python support is not, or QGIS is not there at all β install python3-qgis or re-run the installer. Found but unloadable: Python locates the module and something underneath it is missing β PyQt6 in a plain venv, or the C++ libraries when the dynamic linker cannot find them, the libqgis_core.so and DLL load failed variants.
The third case is the one that catches people who have already "fixed" the first with sys.path.append. Appending to sys.path tells Python where to find qgis/__init__.py; it says nothing to the operating system's dynamic linker about where libqgis_core.so lives, and setting LD_LIBRARY_PATH from inside a running Python is too late for it. That is why environment variables set before the process starts are the reliable route, and why the platform launchers and conda's activation scripts exist at all.
Finally, the reason this matters more for automation than for interactive work: the QGIS Python Console has all of this arranged for it, so code developed there appears to work perfectly and then fails the moment it leaves. Testing the script the way it will actually run β from a terminal with a clean environment, and then under env -i as example 2 does β moves the discovery from the night of the first scheduled run to the afternoon you wrote it.
Sandboxed installs, two versions and cron
pip install qgis fails
There is no qgis or pyqgis package on PyPI; pip reports "No matching distribution found". qgis-stubs does install β it gives an editor autocompletion and nothing that runs.
Flatpak and Snap installs are sandboxed
QGIS installed from Flatpak or Snap runs in a container with its own filesystem view, so an external Python cannot use its bindings. For scripting, install the packages from qgis.org or your distribution, use conda-forge, or work inside the sandbox's own shell.
Two QGIS versions on one machine
Windows in particular ends up with both a release and an LTR installed. Each has its own python-qgis*.bat and its own prefix. Mixing the bindings from one with the libraries of the other produces DLL errors; pick one and use its batch file consistently.
cron does not read your profile
An import that works in your shell and fails on schedule is an environment problem, not a QGIS one. Call a wrapper script that exports everything explicitly, and test it with a stripped environment, as example 2 does.
The upgrade that broke it
Every QGIS release ships its core library under a new name, in a package of its own:
libraries = sorted(Path("/usr/lib").glob("libqgis_core.so.*"))
print([p.name for p in libraries])
packages = subprocess.run(["dpkg-query", "-W", "-f", "${Package} ${Status}\n", "libqgis-core*"],
capture_output=True, text=True).stdout.splitlines()
print([line.split()[0] for line in packages if line.endswith("install ok installed")])
['libqgis_core.so.4.0.2', 'libqgis_core.so.4.0.3', 'libqgis_core.so.4.2.0', 'libqgis_core.so.4.2.1', 'libqgis_core.so.4.2.2']
['libqgis-core4.0.2', 'libqgis-core4.0.3', 'libqgis-core4.2.0', 'libqgis-core4.2.1', 'libqgis-core4.2.2']
Upgrades install the new library without removing the old, so this machine had five releases' worth side by side. Anything that hard-coded a library path or version keeps loading the old one β or breaks when it is finally removed. Prefer the version-agnostic Python location (/usr/share/qgis/python) and pin the QGIS version in the environment that runs scheduled jobs.
import qgis works but import processing does not
Different problem, same family. Processing is a plugin, not part of the core bindings, so /usr/share/qgis/python/plugins has to be on the path separately β the step above shows the difference. See running QGIS Processing algorithms from Python.
Internal links
- How to Automate QGIS with Python (PyQGIS): The Complete Workflow
- How to Run a PyQGIS Script Headless Without Opening QGIS
- How to Run QGIS Processing Algorithms from Python
- PyQGIS vs GeoPandas: Which to Use for GIS Automation
- How to Make a GIS Workflow Reproducible in Python
- GeoPandas Installation Fails: How to Fix Common Errors
FAQ
Can I install PyQGIS with pip?
No. pip install qgis finds no such package. The bindings are compiled against the exact Qt, GDAL, PROJ, QGIS libraries and Python of a particular build, so there is no wheel that could work everywhere; they ship with QGIS. The closest thing to a pip-style install is conda create -c conda-forge qgis, which brings QGIS and matched dependencies into an environment you control β about 3 GB of it.
Why does my script work in the QGIS Python Console but not in the terminal?
The console runs the QGIS interpreter with every path and environment variable already set by the application launcher. Your terminal Python has none of that. Compare sys.executable and sys.path in both β the difference is the fix, and it is usually "use the QGIS Python" rather than "add one path".
How do I use QGIS in a virtual environment?
Create it with python3 -m venv --system-site-packages .venv so the system packages β PyQt6 among them β remain visible, and add PYTHONPATH=/usr/share/qgis/python if your QGIS packages put the bindings there, as the qgis.org 4.2 packages do. A plain venv with the path set failed on PyQt6. Or skip virtualenvs and use a conda environment that contains QGIS.
What does libqgis_core.so: cannot open shared object file mean?
Python found the qgis module but the dynamic linker cannot find the C++ libraries it wraps. Set LD_LIBRARY_PATH to include the directory holding libqgis_core.so* (usually /usr/lib) before starting Python. On Windows the equivalent is the DLL load failed message, and the fix is to launch through python-qgis.bat rather than assembling PATH by hand.
Where are the QGIS Python bindings installed?
On Debian/Ubuntu it depends on the packages: the official qgis/qgis Docker image (QGIS 3.44) had them in /usr/lib/python3/dist-packages/qgis, the qgis.org QGIS 4.2 packages in /usr/share/qgis/python/qgis. In a conda-forge environment they are under $CONDA_PREFIX/share/qgis/python. On Windows they are under the installation's apps\qgis*\python, and on macOS under QGIS.app/Contents/Resources/python.
It works interactively but fails under cron. Why?
cron runs with a minimal environment: no profile, a short PATH, and none of the variables your shell sets. Put every needed export into a wrapper script, use absolute paths throughout, and test the wrapper with a stripped environment before scheduling it.
Should I use conda or the system package?
Use the system package for a workstation where you also run QGIS Desktop β it is simplest and always matches. Use conda-forge, or the qgis/qgis image, for anything automated: the environment is described by a file, pinned to a version and reproducible on another machine. Check which version you get β in September 2026 conda-forge resolved to QGIS 3.44 while qgis.org shipped 4.2 β and run the environment through conda run, since its Python only sees QGIS once activated.