Skip to main content

Packages and __init__.py

If you're new to programming

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.

If you've coded before

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 styleWho should use it
from smartnotes import NoteCode outside the package (main.py, tests, other projects)
from smartnotes.models import NoteCode 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__.py re-exports Dog, so it is available at the package level.
  • C: Works. import animals runs __init__.py, which imports Dog into the animals namespace. animals.Dog is valid.
  • D: Fails. dog.py is inside animals/, not in the project root. Python cannot find dog as 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

Opening Claude Code

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."