How to Fix "ModuleNotFoundError: No module named 'qgis'"

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 install a Python that already has them.

Problem statement

You run a script and get one of these:

ModuleNotFoundError: No module named 'qgis'
ImportError: No module named qgis.core

Or you get past the import and hit its close relatives, which have the same root cause:

ImportError: libqgis_core.so.3.34: cannot open shared object file: No such file or directory
ImportError: DLL load failed while importing QtCore: The specified module could not be found.

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.

Quick answer

pip install qgis does not work and the packages on PyPI with that name are not the QGIS bindings. Do one of these instead:

Diagnosis tree for a failed qgis import: which interpreter, are the bindings on disk, is the shared library path set.
Three questions in order β€” the second one is where most people stop too early.

Linux β€” install QGIS from the official repository and use the system Python:

python3 -c "import qgis.core; print(qgis.core.Qgis.QGIS_VERSION)"

Windows β€” open the OSGeo4W Shell and use its Python:

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:

conda create -n qgis -c conda-forge qgis python=3.11
conda activate qgis
python -c "import qgis.core; print(qgis.core.Qgis.QGIS_VERSION)"

Step-by-step solution

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)
for p in sys.path:
    print("  ", p)

Run the same three 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; yours does not. That difference is the bug.

A virtualenv is the usual culprit. python3 -m venv .venv creates an environment isolated from system site-packages by design, so a system-installed qgis module is deliberately hidden. Either do not use a virtualenv for QGIS work, or create it with access to the system packages:

python3 -m venv --system-site-packages .venv

Check whether the bindings exist on disk

Where the QGIS Python bindings live on Linux, Windows, macOS, and in a conda environment.
Four platforms, four locations β€” and on three of them the bindings are already there when QGIS is.
# Linux
ls /usr/lib/python3/dist-packages/qgis    2>/dev/null || \
ls /usr/share/qgis/python/qgis            2>/dev/null

# macOS
ls /Applications/QGIS.app/Contents/Resources/python/qgis

# conda
ls "$CONDA_PREFIX/share/qgis/python/qgis"
REM Windows
dir "C:\Program Files\QGIS 3.34\apps\qgis-ltr\python\qgis"

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 and including the Python components.

Point Python at the bindings

On Linux, if the bindings are under /usr/share/qgis/python rather than dist-packages, add them:

export PYTHONPATH="/usr/share/qgis/python:/usr/share/qgis/python/plugins:${PYTHONPATH:-}"
export LD_LIBRARY_PATH="/usr/lib:${LD_LIBRARY_PATH:-}"
python3 script.py

Setting this in a wrapper script rather than inside Python is deliberate: sys.path.append fixes the Python import but not the shared library search, which is why an appended path often turns ModuleNotFoundError into libqgis_core.so: cannot open shared object file.

On Windows, do not try to reproduce the environment by hand. QGIS ships batch files that set every variable correctly:

REM From the OSGeo4W Shell
python-qgis-ltr.bat script.py

Look in C:\Program Files\QGIS 3.34\bin\ 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 create -n gis -c conda-forge qgis geopandas python=3.11
conda activate gis

That gives you QGIS, its bindings, GeoPandas, and matched GDAL and PROJ in one prefix, reproducible from an environment.yml, and identical on your laptop and the server. QGIS_PREFIX_PATH is then simply $CONDA_PREFIX. 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:

from qgis.core import QgsApplication, Qgis

print("QGIS", Qgis.QGIS_VERSION)

QgsApplication.setPrefixPath("/usr", True)
app = QgsApplication([], False)
app.initQgis()

print("prefix :", QgsApplication.prefixPath())
print("srs db :", QgsApplication.srsDatabaseFilePath())

from qgis.core import QgsCoordinateReferenceSystem
crs = QgsCoordinateReferenceSystem("EPSG:27700")
print("CRS ok :", crs.isValid())

app.exitQgis()

If the import works but crs.isValid() is False, the prefix path is wrong and QGIS cannot find its CRS database β€” a different problem with a similar-looking symptom, covered in running PyQGIS headless.

Code examples

Example 1: A diagnostic script to run when it breaks

#!/usr/bin/env python3
"""Print everything needed to diagnose a failed QGIS import."""
import os
import sys
from pathlib import Path

print("executable :", sys.executable)
print("version    :", sys.version.split()[0])
print("venv       :", sys.prefix != sys.base_prefix)

for var in ("PYTHONPATH", "QGIS_PREFIX_PATH", "LD_LIBRARY_PATH",
            "PATH", "PROJ_LIB", "GDAL_DATA", "QT_QPA_PLATFORM"):
    print(f"{var:<18}:", os.environ.get(var, "<unset>"))

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("\nbindings on disk:")
for c in CANDIDATES:
    print(("  βœ“ " if Path(c).is_dir() else "  βœ— ") + c)

print("\nimport test:")
try:
    import qgis.core
    print("  βœ“ qgis.core", qgis.core.Qgis.QGIS_VERSION)
except Exception as exc:
    print(f"  βœ— {type(exc).__name__}: {exc}")

Two ticks in the "bindings on disk" section plus a cross on the import test means a path problem. No ticks means an installation problem. That distinction is the whole diagnosis.

Example 2: A wrapper script that sets the environment

#!/usr/bin/env bash
# run.sh β€” run a PyQGIS script with a correct environment
set -euo pipefail

export QGIS_PREFIX_PATH="${QGIS_PREFIX_PATH:-/usr}"
export PYTHONPATH="/usr/share/qgis/python:/usr/share/qgis/python/plugins:${PYTHONPATH:-}"
export LD_LIBRARY_PATH="/usr/lib:${LD_LIBRARY_PATH:-}"
export QT_QPA_PLATFORM=offscreen
export PROJ_LIB=/usr/share/proj
export GDAL_DATA=/usr/share/gdal

exec /usr/bin/python3 "$@"

./run.sh job.py now works from any directory and from cron, which has almost no environment of its own.

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 the Python that ships with QGIS:\n"
        "  Linux   : /usr/bin/python3 with the qgis package installed\n"
        "  Windows : python-qgis-ltr.bat from the OSGeo4W Shell\n"
        "  macOS   : /Applications/QGIS.app/Contents/MacOS/bin/python3\n"
        "  conda   : conda activate <env with conda-forge::qgis>\n"
    )

Example 4: An environment.yml for reproducible automation

name: gis
channels: [conda-forge]
dependencies:
  - python=3.11
  - qgis=3.34          # pin the LTR
  - geopandas
  - pyyaml
  - pytest
conda env create -f environment.yml
conda activate gis
python -c "import qgis.core, geopandas; print('ok')"

Pinning the QGIS version matters more than it looks: algorithm ids and a few parameter names have changed across the 3.x series, 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.

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.

Explanation

Five causes of a failed QGIS import matched to their fixes.
Five causes, five fixes β€” and only one 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 a set of SIP-generated bindings to QGIS's C++ libraries. Those bindings are compiled, and they link against a specific Qt version, a specific GDAL, a specific PROJ, and specific QGIS shared libraries. 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. So the bindings arrive with QGIS, and your job is to run a Python that can see them.

That framing turns the error into a question with three possible answers. Wrong interpreter: the bindings are on the machine and your Python is not the one that knows about them β€” fix by using the QGIS Python or a --system-site-packages virtualenv. Not installed: QGIS is present but its Python support is not, or QGIS is not there at all β€” fix by installing python3-qgis or re-running the installer. Found but unloadable: Python locates the module and the dynamic linker cannot find the C++ libraries underneath β€” that is the libqgis_core.so or DLL load failed variant, fixed by LD_LIBRARY_PATH on Linux or by using the batch file on Windows.

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.3.34 lives. That is why environment variables set before the process starts are the reliable route, and why the platform-specific launcher 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 from cron β€” moves the discovery from the night of the first scheduled run to the afternoon you wrote it.

Edge cases or notes

pip install qgis appears to succeed

There are unrelated packages on PyPI whose names contain qgis. Installing one can shadow the real bindings and produce a confusing ImportError from inside a module that is not QGIS at all. If you have done this, pip uninstall it before debugging further.

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 import its bindings at all. For scripting, install the .deb from the official QGIS repository, use conda-forge, or run 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: env -i HOME="$HOME" PATH=/usr/bin:/bin bash -lc '/srv/gis/run.sh job.py'.

The upgrade that broke it

A QGIS upgrade changes the soname of its libraries β€” libqgis_core.so.3.34 becomes .3.40. Anything that hard-coded a path or a version breaks. Prefer the version-agnostic locations (/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 its directory has to be on sys.path separately β€” usually /usr/share/qgis/python/plugins. See running QGIS Processing algorithms from Python.

FAQ

Can I install PyQGIS with pip?

No. The bindings are compiled against the exact Qt, GDAL, PROJ, and QGIS libraries 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 install -c conda-forge qgis, which brings QGIS and matched dependencies into an environment you control.

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-installed bindings remain visible, or skip virtualenvs entirely and use a conda environment that contains QGIS. A plain venv deliberately hides system packages, which is exactly what breaks the import.

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, /usr/lib/python3/dist-packages/qgis or /usr/share/qgis/python/qgis. On Windows, C:\Program Files\QGIS 3.xx\apps\qgis-ltr\python\qgis. On macOS, /Applications/QGIS.app/Contents/Resources/python/qgis. In a conda environment, under $CONDA_PREFIX/share/qgis/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 for anything automated: the environment is described by a file, pinned to a version, reproducible on another machine, and can hold GeoPandas alongside QGIS in one prefix.