Packages, Namespaces, and Public APIs
Packages, Namespaces, and Public APIs
After reading this chapter, you will organize codebases into hierarchical Python packages, structure __init__.py to create clean public API façades, contrast standard packages with PEP 420 namespace packages, use relative imports correctly, and control exported symbols using __all__.
Mental model
A package is a directory that Python treats as a module containing sub-modules or sub-packages.
Package Hierarchy:
my_service/ ──▶ Package Directory
├── __init__.py ──▶ Marks directory as regular package; defines public API
├── core/ ──▶ Sub-package
│ ├── __init__.py
│ └── engine.py ──▶ Module: my_service.core.engine
└── transports/ ──▶ Sub-package
├── __init__.py
└── http.py ──▶ Module: my_service.transports.http
When importing my_service.core.engine: 1. my_service/__init__.py executes and binds my_service. 2. my_service/core/__init__.py executes and binds core on my_service. 3. my_service/core/engine.py executes and binds engine on my_service.core.
Minimal example
Save as package_facade.py:
# package_facade.py
import tempfile
from pathlib import Path
def create_sample_package(root: Path) -> None:
pkg_dir = root / "cloudpkg"
pkg_dir.mkdir()
# Submodule 1: Internal client
client_code = """
class CloudClient:
def connect(self) -> str:
return "Connected to cloud endpoint."
"""
(pkg_dir / "client.py").write_text(client_code)
# __init__.py creates a clean public API facade and restricts __all__
init_code = """
from .client import CloudClient
__all__ = ["CloudClient"]
"""
(pkg_dir / "__init__.py").write_text(init_code)
def main() -> None:
import sys
with tempfile.TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
create_sample_package(tmp_path)
# Append temporary directory to sys.path to simulate an installed package
sys.path.insert(0, str(tmp_path))
try:
# Consumer imports directly from top-level package facade
import cloudpkg
print(f"Package loaded: {cloudpkg.__name__}")
print(f"Exported symbols (__all__): {cloudpkg.__all__}")
client = cloudpkg.CloudClient()
print(f"Client method invocation : {client.connect()}")
finally:
sys.path.pop(0)
if __name__ == "__main__":
main()Run via uv run python package_facade.py:
Package loaded: cloudpkg
Exported symbols (__all__): ['CloudClient']
Client method invocation : Connected to cloud endpoint.
Worked examples
Case 1: Relative vs Absolute Imports
Within a package, files can refer to sibling or parent modules using dot notation (.):
my_app/
├── models/
│ ├── __init__.py
│ └── user.py
└── services/
├── __init__.py
└── auth.py ──▶ Needs to import User from models
Inside my_app/services/auth.py:
# Absolute import (Recommended for clarity in large apps):
from my_app.models.user import User
# Relative import (Recommended for reusable, relocatable library packages):
from ..models.user import User
from .helpers import local_token_generator # Sibling import from same directory.refers to the current package directory...refers to the parent package directory....refers to the grandparent package directory.
Relative imports rely on the module’s __name__ property containing package context. If you run a file containing relative imports directly with python my_app/services/auth.py, it fails with ImportError: attempted relative import with no known parent package. Always run package modules using the -m flag:
python -m my_app.services.authCase 2: The __all__ Contract and from module import *
The __all__ list specifies which symbols are exported when a user executes wildcard imports (from mypkg import *), and communicates the supported public API to linters and documentation generators:
# api_contract.py
__all__ = ["PublicService", "public_helper"]
class PublicService:
"""Supported public class."""
pass
def public_helper() -> str:
"""Supported public function."""
return "OK"
def _internal_helper() -> None:
"""Private function: prefixed with underscore."""
pass
class InternalEngine:
"""Unexported class: omitted from __all__."""
pass
if __name__ == "__main__":
print(f"Exported public symbols in module: {__all__}")Run:
uv run python api_contract.pyOutput:
Exported public symbols in module: ['PublicService', 'public_helper']
Case 3: PEP 420 Namespace Packages
In modern Python, a directory without an __init__.py file is treated as a namespace package. This enables splitting a single top-level package namespace (e.g. company.core, company.storage, company.auth) across completely separate directories, wheels, or git repositories:
Repo 1 (Core):
company/ (NO __init__.py)
└── core/
└── engine.py
Repo 2 (Plugins):
company/ (NO __init__.py)
└── plugins/
└── exporter.py
When both repositories are installed into site-packages, CPython merges them into a single unified company namespace at runtime!
Pitfalls
Pitfall 1: Executing Submodules Directly Instead of via -m
Running python pkg/sub/file.py sets sys.path[0] to pkg/sub/, stripping package hierarchy context:
# BROKEN:
python my_pkg/submodule.py
# -> ImportError: attempted relative import with no known parent package
# CORRECT:
uv run python -m my_pkg.submodulePitfall 2: Heavy Work Inside __init__.py
__init__.py runs every time any submodule of the package is imported. Putting database connections, expensive file I/O, or heavy third-party imports inside __init__.py slows down every consumer importing lightweight utilities from that package.
Exercises
- Create a package directory structure with an
__init__.pythat exposes a single__version__string and re-exports a class from a nested submodule. - Given a two-level package hierarchy, write a relative import statement from
pkg.controllers.authto import a helper frompkg.utils.crypto. - Create a PEP 420 namespace package with two directories in different locations and demonstrate that both submodules are importable under the same parent namespace.
- Define
__all__in a module and verify that names not listed in__all__are not imported when usingfrom module import *.
Further reading
- PEP 420: Implicit Namespace Packages.
- Python Tutorial: Packages.
- Python Packaging User Guide: Creating and Structuring Projects.