Package Structure — __init__.py, Relative/Absolute Imports, Namespace Packages
A package is a collection of multiple modules grouped into a single directory. It is the foundation for logically organizing code in large projects and distributing reusable libraries.
What Is a Package: Directory + __init__.py
A traditional package requires an __init__.py file inside the directory.
mypackage/
__init__.py
core.py
utils.py
models/
__init__.py
user.py
product.py
# Importing mypackage executes __init__.py
import mypackage
from mypackage import core
from mypackage.models import user
The Role of __init__.py
__init__.py initializes a package and defines its public API.
Empty __init__.py
Simply marks the directory as a package.
# mypackage/__init__.py (empty file is fine)
Defining Public API
# mypackage/__init__.py
"""mypackage — a collection of useful utilities"""
from .core import MainClass, helper_func
from .utils import format_date, parse_config
__version__ = "1.2.0"
__author__ = "Jane Doe"
__all__ = ["MainClass", "helper_func", "format_date", "parse_config"]
This lets users import from the top level without knowing the internal structure:
from mypackage import MainClass, helper_func # No need to know internal paths
Performance Optimization with Lazy Imports
# mypackage/__init__.py
def get_heavy_module():
"""Load the heavy module only on first use"""
from . import heavy_processing
return heavy_processing
Relative Imports: from . import X
Used when referencing other modules within the same package.
# mypackage/core.py
# Absolute import
from mypackage.utils import helper # Must specify package name
# Relative import
from .utils import helper # . = current package
from ..common import shared_func # .. = parent package
from . import models # Import subpackage
project/
mypackage/
__init__.py
core.py # from .utils import helper
utils.py
models/
__init__.py
user.py # from ..utils import helper (go up to parent)
# mypackage/models/user.py
from ..utils import format_date # mypackage.utils.format_date
from ..core import BaseModel # mypackage.core.BaseModel
from . import product # mypackage.models.product
Note: Relative imports can only be used inside packages. They cannot be used in top-level scripts.
Absolute vs Relative Imports
| Absolute Import | Relative Import | |
|---|---|---|
| Form | from mypackage.utils import func | from .utils import func |
| Clarity | Clear path from anywhere | Relative to current location |
| Refactoring | Must update all refs if package name changes | Independent of package name changes |
| Recommended for | Public API, external usage | References between modules inside a package |
PEP 8 recommends absolute imports by default, while allowing relative imports for internal references.
# Recommended: clear absolute import
from mypackage.models.user import User
# Inside a package, relative import is also fine
from .models.user import User
Namespace Packages (PEP 420): Packages Without __init__.py
Since Python 3.3, you can create packages without __init__.py. This is mainly used in large projects where a single namespace is shared across multiple directories.
# Different locations sharing the same namespace
/project_a/myorg/auth/login.py
/project_b/myorg/payments/checkout.py
# Can be imported even without __init__.py
from myorg.auth.login import authenticate
from myorg.payments.checkout import process
import sys
# Add both paths to sys.path
sys.path.extend(["/project_a", "/project_b"])
# Access via namespace package
from myorg.auth import login
from myorg.payments import checkout
In practice, this is used in plugin architectures and monorepo environments.
Package Structure Design Pattern (src layout)
The recommended structure for modern Python projects is the src layout.
my-project/
src/
mypackage/
__init__.py
core/
__init__.py
engine.py
parser.py
utils/
__init__.py
helpers.py
validators.py
cli/
__init__.py
main.py
tests/
__init__.py
test_core.py
test_utils.py
pyproject.toml
README.md
Advantages of src layout:
- Prevents accidentally importing uninstalled packages
- Forces tests to test the installed package
- Works well with
editable install(pip install -e .)
# pyproject.toml
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.backends.legacy:build"
[project]
name = "mypackage"
version = "1.0.0"
[tool.setuptools.packages.find]
where = ["src"] # Find packages in the src directory
Practical Example: Plugin Package Structure
plugin_system/
src/
core/
__init__.py
registry.py
base_plugin.py
plugins/
__init__.py
csv_plugin.py
json_plugin.py
xml_plugin.py
tests/
test_registry.py
# src/core/base_plugin.py
from abc import ABC, abstractmethod
from typing import Any
class BasePlugin(ABC):
"""Base class for all plugins"""
name: str = ""
@abstractmethod
def process(self, data: Any) -> Any:
"""Process data"""
...
@abstractmethod
def validate(self, data: Any) -> bool:
"""Validate input"""
...
# src/core/registry.py
from __future__ import annotations
from typing import Type
from .base_plugin import BasePlugin
__all__ = ["PluginRegistry"]
class PluginRegistry:
"""Plugin registration and management"""
_plugins: dict[str, Type[BasePlugin]] = {}
@classmethod
def register(cls, plugin_cls: Type[BasePlugin]) -> Type[BasePlugin]:
"""Register a plugin via decorator"""
cls._plugins[plugin_cls.name] = plugin_cls
return plugin_cls
@classmethod
def get(cls, name: str) -> Type[BasePlugin]:
if name not in cls._plugins:
raise KeyError(f"Plugin '{name}' is not registered.")
return cls._plugins[name]
@classmethod
def list_all(cls) -> list[str]:
return list(cls._plugins.keys())
# src/core/__init__.py
from .registry import PluginRegistry
from .base_plugin import BasePlugin
__all__ = ["PluginRegistry", "BasePlugin"]
# src/plugins/csv_plugin.py
from ..core import PluginRegistry, BasePlugin
import csv
import io
@PluginRegistry.register
class CsvPlugin(BasePlugin):
name = "csv"
def process(self, data: list[dict]) -> str:
output = io.StringIO()
if data:
writer = csv.DictWriter(output, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
return output.getvalue()
def validate(self, data: list[dict]) -> bool:
return isinstance(data, list) and all(isinstance(row, dict) for row in data)
Expert Tips
Tip 1: Version management in __init__.py
# mypackage/__init__.py
from importlib.metadata import version, PackageNotFoundError
try:
__version__ = version("mypackage")
except PackageNotFoundError:
__version__ = "unknown" # When not installed in development environment
Tip 2: TYPE_CHECKING pattern to prevent circular imports
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .models import User # Not imported at runtime
def get_user() -> "User": # Type hint only
from .models import User # Import at actual usage time
return User()
Tip 3: Clean public API with __all__ and __init__.py
# mypackage/__init__.py
from .core import Engine
from .utils import format_output
from .models import User, Product
# Specify only names to expose to users
__all__ = ["Engine", "format_output", "User", "Product"]
Tip 4: Accessing package data files
from importlib.resources import files
# Read a data file inside a package (Python 3.9+)
config_text = files("mypackage").joinpath("data/default_config.toml").read_text()
template = files("mypackage.templates").joinpath("base.html").read_text()