__init__.py turns directories into packages. Here's what you need to know.

Basic Package Structure

mypackage/
├── __init__.py
├── module_a.py
└── module_b.py

With __init__.py, you can import:

import mypackage
from mypackage import module_a
from mypackage.module_b import something

Empty init.py

The simplest case:

# mypackage/__init__.py
# (empty file)

This just marks the directory as a package. Users import submodules directly:

from mypackage.module_a import func_a

Exporting a Public API

Control what's available at the package level:

# mypackage/__init__.py
from mypackage.module_a import func_a, ClassA
from mypackage.module_b import func_b
 
__all__ = ["func_a", "ClassA", "func_b"]

Now users can do:

from mypackage import func_a, ClassA
# Instead of:
from mypackage.module_a import func_a

all

Defines what from package import * imports:

# mypackage/__init__.py
__all__ = ["public_func", "PublicClass"]
 
def public_func():
    pass
 
def _private_func():
    pass
 
class PublicClass:
    pass
from mypackage import *
# Only imports public_func and PublicClass

Also helps IDE autocompletion and documentation.

Lazy Loading

For large packages, defer imports:

# mypackage/__init__.py
def __getattr__(name):
    if name == "heavy_module":
        from mypackage import heavy_module
        return heavy_module
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

Module only loads when accessed.

Package Initialization Code

Run code when package is imported:

# mypackage/__init__.py
print("Package loaded!")  # Runs on first import
 
# Setup logging
import logging
logging.getLogger(__name__).addHandler(logging.NullHandler())
 
# Version
__version__ = "1.0.0"

Keep initialization minimal—heavy work slows imports.

Subpackages

mypackage/
├── __init__.py
├── sub1/
│   ├── __init__.py
│   └── module.py
└── sub2/
    ├── __init__.py
    └── module.py

Each directory with __init__.py is a package:

from mypackage.sub1 import module
from mypackage.sub2.module import something

Relative Imports

Inside packages, use relative imports:

# mypackage/module_b.py
from .module_a import func_a  # Same package
from ..other_package import something  # Parent package
# . = current package
# .. = parent package
# ... = grandparent package

Common Patterns

Re-export submodules

# mypackage/__init__.py
from mypackage import module_a
from mypackage import module_b
 
# Users can do: mypackage.module_a.func()

Version info

# mypackage/__init__.py
__version__ = "1.0.0"
__author__ = "Owen"

Flatten imports

# mypackage/__init__.py
from mypackage.core import Client, Config
from mypackage.utils import helper
 
# Users get: from mypackage import Client

Without init.py

Since Python 3.3, namespace packages work without __init__.py:

mypackage/
├── module_a.py
└── module_b.py

Still imports work:

from mypackage import module_a

But: explicit __init__.py is still recommended for clarity.

My Rules

  1. Always create __init__.py — explicit is better
  2. Keep it minimal — avoid heavy initialization
  3. Export public API — hide implementation details
  4. Use __all__ — document public interface
  5. Use relative imports — inside packages

A well-designed __init__.py makes packages pleasant to use.

React to this post: