PythonThe Import System

The Import System

Once your project grows past a single file, you'll organize code into packages — directories of modules. Python then needs rules for two things: how an import statement is written (absolute vs relative), and how Python finds the file behind that import (the module search path). This page covers both, plus two problems every Python developer eventually runs into: circular imports and dynamic imports.

Absolute vs relative imports

Consider a small package laid out like this:

Text
mypkg/
    __init__.py
    utils.py
    core.py

Python
# mypkg/utils.py
def helper():
    return "helper result"

Inside core.py, you can reach helper two different ways:

Python
# mypkg/core.py  — absolute import
from mypkg.utils import helper

def run():
    return helper()

Python
# mypkg/core.py  — relative import
from .utils import helper

def run():
    return helper()

Style

Syntax

When to use

Absolute

from mypkg.utils import helper

Clear and unambiguous; preferred by PEP 8 for most cases; works from any file regardless of location

Relative

from .utils import helper / from ..siblingpkg import x

Useful inside large packages to avoid repeating the full package name; only works inside a package, never in a standalone script

Note
The leading dot in a relative import (.) means "this package"; two dots (..) means "the parent package". Relative imports only work inside a module that Python recognizes as part of a package — running that file directly as a script (rather than importing it) will raise an error.
How Python finds modules: sys.path

When you write import something, Python doesn't search your whole hard drive — it checks a specific, ordered list of locations stored in sys.path. You can inspect it yourself:

Python
import sys

for entry in sys.path:
    print(entry)
/home/user/myproject
/usr/lib/python3.11
/usr/lib/python3.11/lib-dynload
/home/user/myproject/venv/lib/python3.11/site-packages

The exact entries vary by machine, but the search order always follows the same general pattern.

  1. Built-in modules — compiled into the Python interpreter itself (e.g. sys, builtins); these are checked before touching the filesystem at all.

  2. The directory containing the script being run (or the current directory in interactive mode) — this is why sibling files in the same folder are importable with no extra setup.

  3. Directories listed in the PYTHONPATH environment variable, if set — searched in the order they appear.

  4. Installation-dependent default paths — the standard library location and the site-packages directory where packages installed via pip live.

The first matching module found along this path wins — which is exactly why naming your own file math.py can shadow the standard library's math module if your directory is searched first.

The role of __init__.py

An __init__.py file inside a directory marks it as a package. Before Python 3.3, this file was required for a directory to be importable as a package; modern Python supports "namespace packages" without it, but including __init__.py is still standard practice.

Beyond just marking a package, __init__.py can contain real code: it runs automatically the first time the package is imported, so it's a common place to set up package-level initialization or to define __all__, a list that controls exactly which names from package import * will expose. (Package structure and __all__ are covered in full depth on the Packages page — this is just the import-system angle.)

Warning
Circular imports. If two modules import each other at the top level, Python can get stuck with a half-initialized module. For example:

a.py
import b
def use_b(): return b.value

b.py
import a
value = 42

Running a.py starts loading a, which immediately tries to import b; while loading b, Python hits import a again — but a is still mid-initialization in sys.modules, so depending on what's accessed you get an ImportError or an AttributeError for a name that "should" exist. The usual fixes are: move the import inside the function that needs it (deferring it until call time, after both modules have finished loading), or restructure the code so the shared logic lives in a third module that both import from instead of importing each other.

Python
# a.py — fixed by importing inside the function
def use_b():
    import b  # deferred import, resolved only when use_b() is called
    return b.value
Dynamic imports with importlib

Sometimes the module name isn't known until runtime — for example, it comes from a config file or user input. The importlib module lets you import by string name instead of writing a literal import statement.

Python
import importlib

module_name = "json"
json_module = importlib.import_module(module_name)

data = json_module.dumps({"status": "ok"})
print(data)
{"status": "ok"}

This is the mechanism behind plugin systems and frameworks that load code by configuration rather than hard-coded imports — use it when the set of modules to import genuinely isn't known ahead of time; for everyday code, a plain import statement is simpler and easier to statically analyze.