Skip to main content
Advertisement

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 ImportRelative Import
Formfrom mypackage.utils import funcfrom .utils import func
ClarityClear path from anywhereRelative to current location
RefactoringMust update all refs if package name changesIndependent of package name changes
Recommended forPublic API, external usageReferences 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()
Advertisement