Skip to main content
Import errors are the most common stumbling block for Python beginners — and for good reason. Python uses a layered system of directories, environment variables, and installation records to locate your code, and a single misplaced file or a wrong working directory is enough to break everything. Once you understand the mental model, though, you will fix these errors in seconds instead of minutes.

The mental model: where am I, where do I want to go?

Every time Python sees an import statement, it asks one question: in which directories should I look for this name? The answer lives in a list called sys.path. Python walks that list from top to bottom and imports the first match it finds.
Run this at the top of any script and you immediately see the full search order. The typical output looks like this:
The three sources that populate sys.path are:
  1. The script’s directory — added automatically when you run python script.py
  2. The standard library — Python’s built-in modules (os, json, datetime, …)
  3. site-packages — third-party packages installed via pip or uv

How virtual environments affect sys.path

When you activate a virtual environment, Python swaps out the site-packages entry to point at the environment’s isolated directory instead of the global one. That is the entire mechanism — nothing more magical than a directory swap.
Always activate your virtual environment before running your project scripts. If you see ModuleNotFoundError for a package you know you installed, a deactivated venv is the most likely culprit.

Absolute vs relative imports

Within a package you can reference other modules in two ways. Always prefer absolute imports — they are unambiguous and work regardless of how you run the file.

The PYTHONPATH environment variable

PYTHONPATH is a colon-separated (or semicolon-separated on Windows) list of directories that Python prepends to sys.path before adding the script directory. It is useful for quickly making your project root importable without installing the package.
Setting PYTHONPATH globally in your shell profile (~/.bashrc, ~/.zshrc) can cause unexpected behaviour across unrelated projects. Prefer per-project solutions like editable installs or the -m flag instead.

Running scripts correctly with the -m flag

The single most reliable way to run a Python module inside a package is the -m flag. It tells Python to run the file as a module, not as a standalone script, which correctly sets up the package hierarchy.
Notice the dot notation (weather_analysis.main) — no .py extension and no slashes.

Editable installs and the src layout

If you use the src/ layout (recommended), your package is not on sys.path by default because it lives inside the src/ wrapper. The cleanest fix is an editable install, which creates a special link so Python can always find your package — even as you edit the source files.
After this, from weather_analysis.data_fetcher import fetch_weather works from anywhere — in scripts, tests, notebooks, and the Python REPL — without setting PYTHONPATH or using sys.path.append.

The __name__ == "__main__" guard

Every Python file has a built-in __name__ variable. When you run a file directly, Python sets __name__ to "__main__". When the same file is imported by another module, __name__ is set to the module’s dotted path instead. Use this to write code that runs only when executed directly:

Diagnosing and fixing import errors

Python searched every directory in sys.path and found nothing named X.Diagnose it:
Common causes and fixes:
This applies to data files (CSV, JSON, images), not to Python modules. Python looked for a file at the path you provided relative to the current working directory, not relative to the script file.
Use pathlib to build paths relative to the script file itself — this works no matter where you run from:
You used a relative import (.module) but ran the file as a standalone script instead of as part of a package.
Or switch the relative import to an absolute one.
File paths use / (or \ on Windows). Module paths use .. They are not interchangeable.
Module A imports from module B, and module B imports from module A. Python partially initialises one of them and you get an ImportError or AttributeError.
Fix circular imports by extracting shared code into a third module (utils.py, models.py) that neither module_a nor module_b imports.

Practical import patterns

Follow the isort convention: group imports as (1) standard library, (2) third-party, (3) local, with a blank line between each group. The ruff linter enforces this automatically — add it to your project with uv add --dev ruff.

Organizing Code

Learn how to split a large script into clean, reusable modules