Packages and __init__.py
A package is a directory that contains Python modules. Adding a special file called __init__.py tells Python "this directory is a package; you can import from it." Packages let you group related modules together, like putting files into folders on your computer.
Python packages are directories with __init__.py. The init file can be empty (just marks the directory as importable) or can re-export names to create a clean public API. This lesson creates the smartnotes/ package and designs its __init__.py.
James has three modules: models.py, search.py, file_manager.py. They sit in the same directory as his test files and his main script. As the project grows, this flat structure becomes hard to navigate.
"Which files are my code and which are tests?" he asks. "Which ones are utilities and which are the core logic?"
Emma draws a directory tree on the whiteboard:
my_project/
├── smartnotes/
│ ├── __init__.py
│ ├── models.py
│ ├── search.py
│ └── storage.py
├── tests/
│ ├── test_models.py
│ ├── test_search.py
│ └── test_storage.py
└── main.py
"This is a package," she says. "The smartnotes/ directory groups all your code. The tests/ directory groups all your tests. main.py ties them together."
From Flat Files to a Package
Before (flat structure):
project/
├── models.py
├── search.py
├── file_manager.py
├── test_search.py
├── test_file_manager.py
└── main.py
After (package structure):
project/
├── smartnotes/
│ ├── __init__.py
│ ├── models.py
│ ├── search.py
│ └── storage.py
├── tests/
│ ├── test_search.py
│ └── test_storage.py
└── main.py
The difference: your code lives inside a smartnotes/ directory, and that directory has a file called __init__.py.
Creating Your First Package
Create the directory and the init file:
mkdir smartnotes
Create smartnotes/__init__.py (it can be empty for now):
"""SmartNotes: a note-taking application."""
Move models.py into the smartnotes/ directory. Now the structure is:
project/
├── smartnotes/
│ ├── __init__.py
│ └── models.py
└── main.py
Update main.py to import from the package:
from smartnotes.models import Note
note = Note("Python Tips", "Learn basics", 3, "James", tags=["python"])
print(note)
Run it:
uv run python main.py
Output:
Note(title='Python Tips', body='Learn basics', word_count=3, author='James', is_draft=True, tags=['python'])
The import changed from from models import Note to from smartnotes.models import Note. The dot (.) separates the package name from the module name. Read it as: "from the smartnotes package, open the models module, and get Note."
What __init__.py Does
__init__.py serves two purposes:
1. It marks a directory as a Python package. Without it, Python does not recognize the directory as importable. Always include __init__.py in your package directories.
2. It runs when the package is first imported. Any code in __init__.py executes when someone writes import smartnotes or from smartnotes import .... This makes it the right place to define what the package exports.
Designing the Public API
Right now, importing Note requires knowing the internal structure: from smartnotes.models import Note. That exposes an implementation detail. If you rename models.py to data.py, every file that imports from it breaks.
A better approach: re-export key names in __init__.py so users import from the package, not from internal modules.
Update smartnotes/__init__.py:
"""SmartNotes: a note-taking application."""
from smartnotes.models import Note
from smartnotes.search import search_notes
__all__ = ["Note", "search_notes"]
Now main.py can use the simpler import:
from smartnotes import Note, search_notes
notes = [
Note("Python Tips", "Learn basics", 2, "James", tags=["python"]),
Note("Debugging", "Fix errors", 2, "James", tags=["debug"]),
]
results = search_notes(notes, "python")
print(f"Found {len(results)} notes")
Output:
Found 1 notes
The import is from smartnotes import Note, not from smartnotes.models import Note. Users of your package do not need to know that Note lives in models.py. If you reorganize the internals later, the public import stays the same.
| Import style | Who should use it |
|---|---|
from smartnotes import Note | Code outside the package (main.py, tests, other projects) |
from smartnotes.models import Note | Code inside the package (search.py importing from models.py) |
__all__: What Gets Exported
The __all__ variable is a list of names that the package exports. It serves two purposes: it documents the public API and it controls what from smartnotes import * imports.
__all__ = ["Note", "search_notes"]
This says: "The public interface of smartnotes is the Note class and the search_notes function. Everything else is internal."
Here is what happens with and without __all__:
# __init__.py WITHOUT __all__:
from smartnotes.models import Note
from smartnotes.search import search_notes, filter_by_tag
# from smartnotes import * → imports Note, search_notes, AND filter_by_tag
# (everything that was imported into __init__.py)
# __init__.py WITH __all__:
from smartnotes.models import Note
from smartnotes.search import search_notes, filter_by_tag
__all__ = ["Note", "search_notes"]
# from smartnotes import * → imports ONLY Note and search_notes
# filter_by_tag is still available via smartnotes.search.filter_by_tag
# but it is not part of the public API
You should rarely use from package import * in your own code. But defining __all__ is still good practice because it clearly communicates which names are intended for external use.
Adding More Modules to the Package
Move search.py and file_manager.py (renamed to storage.py) into the smartnotes/ directory:
project/
├── smartnotes/
│ ├── __init__.py
│ ├── models.py
│ ├── search.py
│ └── storage.py
├── tests/
│ ├── test_search.py
│ └── test_storage.py
└── main.py
Update search.py to import from within the package:
"""SmartNotes search and filter operations."""
from smartnotes.models import Note
def search_notes(
notes: list[Note],
keyword: str,
tag: str | None = None,
) -> list[Note]:
"""Search notes by keyword with optional tag filter."""
...
Update storage.py similarly:
"""SmartNotes file I/O operations."""
from smartnotes.models import Note
class FileManager:
"""Manages SmartNotes file I/O across JSON, CSV, and Markdown."""
...
Update __init__.py to export the new names:
"""SmartNotes: a note-taking application."""
from smartnotes.models import Note
from smartnotes.search import search_notes
from smartnotes.storage import FileManager
__all__ = ["Note", "search_notes", "FileManager"]
Now main.py imports everything from one place:
from smartnotes import Note, search_notes, FileManager
PRIMM-AI+ Practice: Predict the Import Path
Predict [AI-FREE]
Press Shift+Tab to enter Plan Mode.
Given this structure:
project/
├── animals/
│ ├── __init__.py # contains: from animals.dog import Dog
│ └── dog.py # contains: class Dog: ...
└── main.py
Which of these imports work in main.py?
# A
from animals.dog import Dog
# B
from animals import Dog
# C
import animals
d = animals.Dog("Rex")
# D
from dog import Dog
Write your answers (works / fails) for A, B, C, D.
Check your predictions
- A: Works. Direct import from the module inside the package.
- B: Works.
__init__.pyre-exportsDog, so it is available at the package level. - C: Works.
import animalsruns__init__.py, which importsDoginto theanimalsnamespace.animals.Dogis valid. - D: Fails.
dog.pyis insideanimals/, not in the project root. Python cannot finddogas a standalone module.
Run
Press Shift+Tab to exit Plan Mode.
Create the animals/ package with __init__.py and dog.py. Test all four imports in separate scripts to confirm your predictions.
Investigate
For each import (A, B, C, D) from the Predict exercise, write one sentence explaining why it works or fails. Check your explanations against the answers.
If you want to go deeper, run /investigate @smartnotes/__init__.py in Claude Code and ask: "What happens if __init__.py is empty? What is the difference between an empty __init__.py and one with re-exports?"
Modify
Add a cat.py module to the animals/ package with a Cat class. Update __init__.py to re-export both Dog and Cat. Verify with from animals import Dog, Cat.
Make [Mastery Gate]
Create the smartnotes/ package with three modules: models.py, search.py, and storage.py. Write an __init__.py that exports Note, search_notes, and FileManager. Verify:
uv run pyright smartnotes/
uv run python -c "from smartnotes import Note, search_notes, FileManager; print('All imports OK')"
Try With AI
If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.
Prompt 1: Review My Package Structure
Here is my SmartNotes package structure:
smartnotes/
├── __init__.py
├── models.py
├── search.py
└── storage.py
And here is my __init__.py:
[paste __init__.py]
Is this a good package design? What would you change?
Are there any modules that should be split further or
merged?
What you're learning: Package design is subjective. The AI offers structural opinions and you evaluate whether they fit your project's scale and goals.
Prompt 2: Understand all
What happens if I don't define __all__ in __init__.py?
Does it change what I can import? Show me an example
where having __all__ prevents a problem.
What you're learning: __all__ is documentation and protection. Without it, from smartnotes import * pulls in everything, including internal helpers. With it, you control the public surface.
Prompt 3: Real-World Package Examples
Show me the package structure of a well-known Python
library (like requests or flask). How do they organize
their __init__.py? What can I learn from their approach?
What you're learning: Studying real packages teaches patterns. Professional libraries use __init__.py to create clean, versioned APIs. You are learning the same conventions.
James looks at his project. The smartnotes/ directory contains three modules and an __init__.py that exports the public API. The tests/ directory holds the test files. main.py imports from the package.
"At the warehouse, we organized the floor by zone: receiving, storage, shipping," he says. "Each zone had its own processes and its own equipment. But the manifest system was shared: every zone could look up any item. The __init__.py is like the manifest system. It tells everyone what is available without exposing where each item is stored."
Emma looks at the imports. "Notice what you are NOT importing: the internal helper functions, the private conversion utilities, the implementation details. Users of your package see Note, search_notes, FileManager. The rest is hidden."
"But my tests need to import internal functions," James says. "How do I test filter_by_tag if it is not in __init__.py?"
"Tests import directly: from smartnotes.search import filter_by_tag. They are inside the project; they are allowed to see the internals. External users go through __init__.py."
She pauses. "There is one more import concept you need: what happens when you run a package as a script. And what __name__ == '__main__' actually means. That is Lesson 3."