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:

Diagnosing a failed qgis import: interpreter, bindings on disk, library path. the three answers, in orderWhich Python is running, and can it seethe bindings?cannot see themSet the pathPYTHONPATH / conda runnot on diskInstall the bindingspython3-qgis / condafound, won’t loadFix the library pathLD_LIBRARY_PATH / .batOnly the middle one involves installing anything spatialworkflow.io
Three questions in order β€” the second one is where most people stop too early.

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

Where the QGIS Python bindings live on each platform. same bindings, different homesinterpreter to usebindings live inqgis.org 4.2 .debpython3 + PYTHONPATH/usr/share/qgis/pythonqgis/qgis:ltr 3.44/usr/bin/python3python3/dist-packages/qgisconda-forge 3.44conda run -p <env>$CONDA_PREFIX/share/qgis/pythonWindowspython-qgis-ltr.batapps\qgis-ltr\pythonmacOSQGIS.app/…/bin/python3Contents/Resources/pythonOnly the Docker image put them where a bare python3 looks; Windows and macOS not run here spatialworkflow.io
Three Linux installs, three locations β€” only the Docker image put the bindings where a bare python3 looks.
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.

The outcome of import qgis.core in six interpreter setups, with the qgis.org QGIS 4.2 packages. one import, six setupsinterpreterno PYTHONPATHPYTHONPATH set/usr/bin/python3no module 'qgis'QGIS 4.2.2venvno module 'qgis'no module 'PyQt6'venv --system-site-packagesno module 'qgis'QGIS 4.2.2PYTHONPATH=/usr/share/qgis/python; add /plugins for import processingThe path finds qgis; the system site-packages supply PyQt6 β€” a working setup needs both spatialworkflow.io
The step-one matrix in a table: neither the path nor the system packages is enough on its own.

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

Five causes of a failed QGIS import matched to their fixes. five causes of one messageCauseFixbindings in /usr/share/qgis/pythonPYTHONPATH, set before Pythonstartsa plain venv: No module namedPyQt6venv --system-site-packages +PYTHONPATHlibqgis_core.so cannot be foundset LD_LIBRARY_PATH before Pythona conda env's python, notactivatedconda run -p <env>works in a shell, fails in crona wrapper that exports everythingNone of the five is fixed by installing anything spatialworkflow.io
Five causes, five fixes β€” and none of them involves installing anything.

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.

Why appending to sys.path fixes one of the three causes and not the third. Python found it; the linker did notsys.path.append("/usr/share/qgis/python")Β Β Β # not enoughexport LD_LIBRARY_PATH=/opt/qgis/libΒ Β Β # before Python startsexport PYTHONPATH=/usr/share/qgis/pythonβ‘ β‘‘β‘ Tells Python where qgis/__init__.py lives. It says nothing to theoperating system about where libqgis_core.so lives.β‘‘Read by the dynamic linker when the process starts, so setting it insidePython is already too late. That is why launcher scripts exist.An ImportError naming a .so file, or "DLL load failed", is the linker rather than Python spatialworkflow.io
A QGIS in a standard prefix needs only PYTHONPATH; one under /opt needs both, set before Python starts.

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

The libqgis_core shared libraries installed on one Ubuntu machine after five QGIS upgrades. five releases, five libraries, all still installedlibqgis_core.so.4.2.2in uselibqgis_core.so.4.2.1left behindlibqgis_core.so.4.2.0left behindlibqgis_core.so.4.0.3left behindlibqgis_core.so.4.0.2left behindCode that names a library version keeps loading an old one for as long as it stays installed spatialworkflow.io
The upgrade entry below, measured: nothing removed the old libraries.

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.

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.