Audience: developers creating small-to-medium Python projects who want clear guidance on when and how to use __init__.py.
Format: explanation, practical examples, recommended patterns, and a short checklist for printing.
__init__.py is the file placed inside a directory to make that directory behave like a Python package. It can be an empty marker file, or it can contain light initialization code, metadata (like __version__), and re-exports that shape the package's public API. Keep it small and avoid heavy side effects (no long-running tasks, no training loops, no large I/O) — importing a package should be cheap and predictable.
- Historically (pre-Python 3.3): A directory required an
__init__.pyfor Python to treat it as an importable package. - Modern Python (PEP 420): Implicit namespace packages are allowed — a directory can be treated as a package without
__init__.py.
However, many projects still include__init__.pyfor clarity, backwards compatibility, and to hold package-level code or metadata.
When you only need the directory to be importable and want to avoid any package-level code.
# src/__init__.py
# empty file or minimal docstring
"""Project `src` package."""Effect
import src # works
from src import train # will import submodule when accessibleExpose a version string that other code and tests can read.
# src/__init__.py
"""churn-project package."""
__version__ = "0.1.0"Usage:
import src
print(src.__version__) # "0.1.0"Re-export frequently used names so callers can import from the package root. Be careful: re-exporting may import submodules at package import time and can cause circular imports or slow startup.
# src/__init__.py
from .train import main # re-export entrypoint
__all__ = ["main"]Then users can do:
from src import main
main(...)Caution: if train.py executes heavy work on import, this will run during import src — avoid that.
Define what symbols from src import * exposes.
# src/__init__.py
__all__ = ["preprocess", "train"]This does not automatically import preprocess and train; it controls only what is exported if those names are present in the package namespace.
If you want convenience re-exports without the import-time cost, use lazy imports:
# src/__init__.py
def _lazy_import_train():
from .train import main
return main
def main(*args, **kwargs):
return _lazy_import_train()(*args, **kwargs)
__all__ = ["main"]This keeps import src cheap and only loads train when src.main() is called.
You may run very small, safe initialization (e.g., set up logging defaults or import-time checks). Avoid I/O and network calls.
# src/__init__.py
import logging
logging.getLogger(__name__).addHandler(logging.NullHandler())
__all__ = []# src/__init__.py
"""churn-project package."""
__version__ = "0.1.0"
__all__ = []# src/__init__.py
"""churn-project package."""
from .train import main # ok if train.py does not run heavy code at import
__all__ = ["main"]
__version__ = "0.1.0"# src/__init__.py
"""churn-project package."""
__version__ = "0.1.0"
def main(*args, **kwargs):
# import only when used
from .train import main as _main
return _main(*args, **kwargs)
__all__ = ["main"]- Long training loops, heavy computations, or long file reads.
- Network calls, opening large database connections, starting servers.
- Anything that has side effects users won't expect on a simple
import.
If you need initialization that is expensive, provide an explicit function (e.g., initialize_env()) and call it intentionally in your CLI or application entrypoint.
setuptools.find_packages()discovers packages by the presence of__init__.py. (You can still use namespace packages, but including__init__.pyis simple and explicit.)- If you publish to PyPI and want
__version__available to consumers, keeping it in__init__.pyis conventional. Another option is to source it from a single place (e.g.,src/_version.py) to avoid import-time side effects.
- Tests can import
__version__to assert package version. - Keep
__init__.pyimport-time behaviour simple so test discovery and linting tools (mypy, pytest) are fast and deterministic. - If linters or type-checkers import your package during checks,
__init__.pyshould not run expensive or fragile code.
- Circular imports: if
__init__.pyimports submodules that import the package back, you will get import errors. Use lazy imports or move shared functions to a small module that both can import. - Slow imports: profile import time (e.g.,
python -X importtime -c "import src") and move heavy code out of__init__.py. - Namespace packages: if you intentionally use PEP 420 implicit namespaces (multiple directories mapped to same package), omit
__init__.py. Otherwise include it for compatibility.
- Does the package need to run code at import? If yes, can it be deferred?
- Is there a clear
__version__you want accessible? Put it here. - Do you want to expose a small public API (few names)? Re-export carefully or use lazy imports.
- Are there any possible circular imports? Test imports in a fresh REPL.
- Keep file small and free of heavy I/O or network side effects.
Include a small __init__.py with only lightweight metadata (docstring and __version__), avoid automatic heavy work, and use lazy re-exports if you need convenience names without import-time cost.
Empty
# src/__init__.py
"""churn-project package."""Version only
# src/__init__.py
"""churn-project package."""
__version__ = "0.1.0"Safe re-export (lazy)
# src/__init__.py
"""churn-project package."""
__version__ = "0.1.0"
def main(*args, **kwargs):
from .train import main as _main
return _main(*args, **kwargs)
__all__ = ["main"]