Packages, Namespaces, and Public APIs

Updated

September 7, 2026

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.auth

Case 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.py

Output:

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.submodule

Pitfall 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

  1. Create a package directory structure with an __init__.py that exposes a single __version__ string and re-exports a class from a nested submodule.
  2. Given a two-level package hierarchy, write a relative import statement from pkg.controllers.auth to import a helper from pkg.utils.crypto.
  3. Create a PEP 420 namespace package with two directories in different locations and demonstrate that both submodules are importable under the same parent namespace.
  4. Define __all__ in a module and verify that names not listed in __all__ are not imported when using from module import *.

Further reading

  • PEP 420: Implicit Namespace Packages.
  • Python Tutorial: Packages.
  • Python Packaging User Guide: Creating and Structuring Projects.