Introduction
As Python projects grow beyond a single file, organizing code into packages becomes essential. A package is a directory containing an __init__.py file (regular package) or no __init__.py at all (namespace package). Understanding how packages, relative imports, and re-exporting work is critical for building maintainable APIs.
Key Concepts
- Regular package: A directory with an
__init__.pyfile that Python treats as a package. __init__.py: Executed when the package is imported; controls what the package exposes.- Relative imports: Imports using dots (
.) to reference sibling or parent modules within the same package. - Namespace package: A package without
__init__.py(PEP 420), allowing a single logical package to span multiple directories. - Re-exporting: Importing names in
__init__.pyso consumers can access them from the package root.
Real World Context
Consider the requests library structure: users write from requests import get, Session even though get lives in requests.api and Session lives in requests.sessions. This is possible because requests/__init__.py re-exports these names, providing a clean, flat public API while keeping the internal structure organized.
Deep Dive
Basic package structure
mylib/
├── __init__.py # Package entry point
├── models.py # Data models
├── services.py # Business logic
└── _helpers.py # Internal utilities
python# mylib/__init__.py """MyLib - a clean public API.""" from .models import User, Product from .services import create_user, fetch_products __all__ = ['User', 'Product', 'create_user', 'fetch_products']
Relative imports
python# mylib/services.py from .models import User # Same-level import from ._helpers import validate # Same-level private module from ..config import settings # Parent package import def create_user(name: str) -> User: validate(name) return User(name=name)
Nested package structure
mylib/
├── __init__.py
├── api/
│ ├── __init__.py
│ ├── v1.py
│ └── v2.py
├── core/
│ ├── __init__.py
│ └── engine.py
└── utils/
├── __init__.py
└── formatting.py
python# mylib/api/__init__.py from .v2 import router # Default to latest version # mylib/core/engine.py from ..utils.formatting import format_output # Cross-package relative import
Namespace packages (PEP 420)
Namespace packages allow a single package to be split across multiple directories or distributions:
# Directory A (installed by package-a)
mynamespace/
subpackage_a/
__init__.py
module_a.py
# Directory B (installed by package-b)
mynamespace/
subpackage_b/
__init__.py
module_b.py
python# Both work because mynamespace is a namespace package (no __init__.py) import mynamespace.subpackage_a import mynamespace.subpackage_b
Re-exporting for clean APIs
python# mylib/__init__.py # Re-export from submodules so users write: # from mylib import User, create_user # instead of: # from mylib.models import User # from mylib.services import create_user from .models import User, Product from .services import create_user __all__ = ['User', 'Product', 'create_user']
Common Pitfalls
- Circular imports: Module A imports from Module B, which imports from Module A. Fix by moving shared types to a separate module or using local imports.
- Missing
__init__.py: Without it, Python 3 treats the directory as a namespace package, which may cause unexpected import behavior. - Relative import outside package: Running
python mylib/services.pydirectly fails with relative imports. Usepython -m mylib.servicesinstead.
Best Practices
- Keep
__init__.pyfiles small — they should primarily re-export, not contain business logic. - Use relative imports within a package for portability (the package can be renamed without changing internal imports).
- Combine re-exporting with
__all__to create a curated, flat public API. - Avoid deep nesting — if your imports look like
from mylib.core.utils.internal.helpers import x, consider restructuring.
Summary
Python packages are directories with __init__.py files that organize code into logical units. Relative imports keep packages self-contained, re-exporting in __init__.py creates clean public APIs, and namespace packages enable multi-distribution packages. Together, these features let you build well-structured libraries with clear boundaries between public and private code.
Code Examples
# project/payments/__init__.py
"""Payments package - clean public API."""
from .processor import process_payment, refund
from .models import PaymentResult, PaymentStatus
from ._gateway import _StripeGateway # noqa: F401 (internal)
__all__ = [
'process_payment',
'refund',
'PaymentResult',
'PaymentStatus',
]
# Consumer code:
# from payments import process_payment, PaymentResult
# result: PaymentResult = process_payment(amount=99.99)# project/payments/processor.py
from .models import PaymentResult, PaymentStatus
from ._gateway import _StripeGateway
from ..config import settings # Parent package
def process_payment(amount: float) -> PaymentResult:
gateway = _StripeGateway(api_key=settings.STRIPE_KEY)
success = gateway.charge(amount)
return PaymentResult(
status=PaymentStatus.SUCCESS if success else PaymentStatus.FAILED,
amount=amount,
)