Packages and __init__.py
A package is a directory of modules. __init__.py marks it and defines its public face, and relative imports let modules inside it find each other.
- From module to package
- __init__.py
- 1. Defining the public surface
- 2. Package level constants
- 3. Keeping it empty
- Nested packages
- Relative imports
- Relative or absolute?
- The error everyone meets
- Making a package runnable
- A realistic layout
- Import cost
- Inspecting a package
- Common mistakes
- Best practices
- Practice
- Conclusion
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
From module to package
A module = one .py file
A package = a directory containing modulesnotesapp/
__init__.py
storage.py
formatting.py
search.pyimport notesapp.storage
from notesapp import formatting
from notesapp.search import find
notesapp.storage.save("a note")
print(formatting.title("hello"))__init__.py
The file __init__.py runs when the package is first imported. It may be completely empty, and often is. Its three real uses are:
1. Defining the public surface
# File: notesapp/__init__.py
"""A small note taking library."""
from .storage import save, load
from .formatting import title
from .search import find
__version__ = "1.0.0"
__all__ = ["save", "load", "title", "find"]import notesapp
notesapp.save("a note") # instead of notesapp.storage.save
print(notesapp.__version__)Users of the package now import from one place. The internal file layout can be reorganised later without breaking anyone.
2. Package level constants
# File: notesapp/__init__.py
from pathlib import Path
PACKAGE_ROOT = Path(__file__).resolve().parent
DEFAULT_STORE = PACKAGE_ROOT / "data" / "notes.txt"
MAX_TITLE_LENGTH = 1203. Keeping it empty
An empty __init__.py is a perfectly good choice. It marks the directory as a package and leaves users to import submodules explicitly, which keeps import cost low.
Since Python 3.3 a directory without__init__.pycan still be imported, as a "namespace package". Include the file anyway: it makes the intent explicit, it makes tooling behave predictably, and it gives you somewhere to put__all__later.
Nested packages
notesapp/
__init__.py
core/
__init__.py
storage.py
models.py
text/
__init__.py
formatting.py
search.py
cli/
__init__.py
__main__.pyimport notesapp.core.storage
from notesapp.core import models
from notesapp.text.formatting import title
from notesapp.core.storage import save as save_noteEvery directory in the chain needs its own __init__.py. The dotted path mirrors the folder structure exactly.
Relative imports
# File: notesapp/text/search.py
from .formatting import title # same package: notesapp.text
from ..core.storage import load # up one level, then into core
from ...other import thing # up two levels
# Absolute equivalents
from notesapp.text.formatting import title
from notesapp.core.storage import load| Syntax | Means |
|---|---|
from . import x | The current package |
from .module import x | A module in the current package |
from .. import x | The parent package |
from ..sub import x | A sibling package |
Relative or absolute?
| Relative | Absolute |
|---|---|
| Short inside a deep package | Longer but unambiguous |
| Survives renaming the top package | Says exactly where a name comes from |
| Only works inside a package | Works anywhere |
| Good for tightly coupled siblings | PEP 8's recommended default |
Use absolute imports by default, and relative imports for modules that clearly belong together inside one subpackage. Be consistent within a project.
The error everyone meets
$ python notesapp/text/search.py
ImportError: attempted relative import with no known parent packageRunning a file inside a package by path means Python does not know it is part of a package, so relative imports have nothing to be relative to. Run it as a module instead:
$ python -m notesapp.text.search # from the directory ABOVE notesappMaking a package runnable
# File: notesapp/__main__.py
import sys
from .core.storage import load
from .text.formatting import title
def main(argv=None):
argv = sys.argv[1:] if argv is None else argv
if not argv:
print("usage: python -m notesapp COMMAND", file=sys.stderr)
return 2
if argv[0] == "list":
for note in load():
print(title(note))
return 0
print(f"unknown command: {argv[0]}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())$ python -m notesapp listA realistic layout
project/
README.md
notesapp/
__init__.py public API and version
__main__.py python -m notesapp
core/
__init__.py
models.py the data structures
storage.py reading and writing
text/
__init__.py
formatting.py
search.py
errors.py the exception hierarchy
tests/
test_storage.py
test_search.py
data/
notes.txt# File: notesapp/errors.py
class NotesError(Exception):
"""Base class for every error raised by notesapp."""
class StorageError(NotesError):
"""Reading or writing notes failed."""
class NoteNotFound(NotesError):
"""The requested note does not exist."""One errors.py holding the whole exception hierarchy is a pattern worth adopting. Every other module imports from it, and callers catch NotesError to cover the entire package.
Import cost
# File: notesapp/__init__.py
# Eager: everything loads the moment anyone imports notesapp
from .core.storage import save, load
from .text.search import find
# Lazy: loaded only when actually used
def find(*args, **kwargs):
from .text.search import find as _find
return _find(*args, **kwargs)A heavy __init__.py makes every import of the package slow, even for a program that needs one function. For a small package, eager imports are simpler and fine. For a large one, keep __init__.py thin.
Inspecting a package
import json
print(json.__name__) # json
print(json.__package__) # json
print(json.__path__) # the directory - only packages have this
print(json.__file__) # .../json/__init__.py
import math
print(hasattr(math, "__path__")) # False - math is a module, not a packageCommon mistakes
- Forgetting
__init__.pyin a nested directory and getting confusing import failures. - Running a file inside a package by path and meeting the relative import error.
- Importing the package name from inside the package itself, creating a circular import.
- Putting heavy work in
__init__.py, so every import is slow. - Mixing relative and absolute imports inconsistently across one project.
- Naming a package after a standard library module.
Best practices
- Include
__init__.pyin every package directory, even when empty. - Use
__init__.pyto define a small, stable public API with__all__. - Prefer absolute imports; use relative imports only within a tight subpackage.
- Run package code with
python -m package.module, never by file path. - Keep one module for the exception hierarchy.
- Keep tests outside the package, in their own directory.
Practice
- Build a package with two subpackages and import a function from each, both ways.
- Write an
__init__.pythat re-exports three functions and set__all__. - Trigger the relative import error deliberately, then fix it with
-m. - Add a
__main__.pyso your package runs as a command. - Add an
errors.pywith a base exception and two subclasses, and use it from two modules.
Conclusion
A package is a directory of modules with an __init__.py that defines what the outside world sees. Use absolute imports by default, relative imports within a subpackage, and run package code with python -m so relative imports resolve.