Modules, Imports & Packages
Python organizes code into modules (files) and packages (directories). The import system loads code once per process, caches it in sys.modules, and exposes names through the importing namespace.
Recipe
# myapp/services/users.py
from myapp.models.user import User
from myapp.utils.text import slugify
def list_active() -> list[User]:
return [u for u in User.all() if u.active]When to reach for this:
- Splitting a growing script into maintainable packages
- Publishing reusable libraries on PyPI
- Running CLI entry points via
python -m - Lazy-loading heavy optional dependencies
Working Example
# myapp/__init__.py
__version__ = "1.0.0"
# myapp/config.py
from dataclasses import dataclass
import os
@dataclass(frozen=True)
class Settings:
debug: bool
database_url: str
def load_settings
Run with: python -m myapp.main from the parent directory on PYTHONPATH.
What this demonstrates:
- Package
__init__.pycan expose version metadata - Absolute imports (
from myapp.config import ...) stay clear when package grows if __name__ == "__main__"entry guard for runnable modules-mswitch sets__package__correctly for relative imports inside the package
Deep Dive
How It Works
- Import search path -
sys.pathlists directories;PYTHONPATHand venv site-packages extend it. - Module cache - First import executes module body; later imports reuse
sys.modules. - Relative imports -
from .config import load_settingsrequire package context (-mor package import). - Namespace packages - PEP 420 directories without
__init__.pycan participate in a split namespace. __all__- Documents public names forfrom module import *(rare in app code).
Layout Recommendation
project/
src/
myapp/
__init__.py
main.py
config.py
pyproject.toml
Install editable (uv pip install -e .) so import myapp resolves during development.
Python Notes
import importlib
def lazy_import(module: str):
return importlib.import_module(module)
# TYPE_CHECKING block avoids runtime circular import
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from myapp.models import UserGotchas
- Circular imports -
aimportsbwhilebimportsa. Fix: Move shared types to a third module or lazy-import inside functions. - Running file directly -
python myapp/main.pybreaks relative imports. Fix:python -m myapp.main. - Shadowing stdlib - Naming a file
json.pybreaksimport json. Fix: Avoid stdlib names for modules. - Side effects at import - DB connections at module level slow tests. Fix: Factory functions called from
main. - Star imports -
from utils import *pollutes namespace. Fix: Import explicit symbols.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Single module script | <200 lines, one author | Multiple domains emerging |
src layout package | Libraries and services | Throwaway notebook |
Lazy importlib | Optional heavy deps (torch) | Core dependency always needed |
pluggy entry points | Plugin architectures | Simple internal package |
FAQs
What is the difference between import and from import?
import module binds the module object. from module import name binds the name directly into the current namespace.
Do I need __init__.py?
Regular packages yes (or implicit namespace package without it). Empty __init__.py is fine for marking a package.
How does python -m help?
Runs a module as __main__ with correct package metadata so relative imports work.
What is sys.path[0]?
The directory containing the script or the current directory for -m. Editable installs add src to path via metadata.
How do I fix ImportError after refactor?
Check package on PYTHONPATH, use absolute imports, verify pyproject.toml package discovery, reinstall editable.
Can I import from parent directory?
Possible with path hacks - discouraged. Install package editable or configure proper layout instead.
What does if TYPE_CHECKING do?
Imports for type checkers only - avoids runtime circular import while keeping hints.
How do relative imports work?
One dot is current package; two dots is parent. Requires package context - not in top-level scripts.
When is import * acceptable?
Rarely - sometimes in __init__.py re-export APIs with explicit __all__. Never in application modules.
How do namespace packages differ?
Multiple directories contribute to one package name without a single __init__.py - used for plugin ecosystems.
Related
- Virtual Environments Quickstart - isolated dependencies
- src layout - production package layout
- pyproject.toml explained - package metadata
- Files & I/O - reading config files
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.