Why Modules?
A module in Python is simply a .py file. When you write import json, Python loads the json.py file from its standard library. This lesson teaches you to create your own modules: you put code in a file and import it from another file. This is how real projects organize code.
Python modules are files. Packages are directories. This lesson covers the basic import and from...import mechanics with SmartNotes as the codebase being split. No __init__.py yet; that is Lesson 2.
James opens his SmartNotes project folder. He counts the lines:
smartnotes_search.py -- 85 lines (Note class + search functions)
file_manager.py -- 120 lines (FileManager class + helpers)
test_smartnotes_search.py -- 95 lines
test_file_manager.py -- 110 lines
"Four hundred lines across four files," he says. "But the Note class is defined in smartnotes_search.py and also needed in file_manager.py. Right now I am copying the class definition into both files."
"That is the problem," Emma says. "Duplicated code. Change the Note class in one file and the other file still has the old version. Tests pass in one file and fail in the other."
"So what do I do?"
"Move Note to its own file. Import it everywhere."
Every .py File Is a Module
You have already used modules. Every import statement in your code loads a module:
import json # loads json.py from the standard library
import csv # loads csv.py from the standard library
from pathlib import Path # loads pathlib.py and gets the Path class
A module is a .py file. That is the entire concept. When you write import json, Python finds a file called json.py, runs it, and makes its contents available to your code.
You can create your own module the same way. Put code in a .py file, and any other file can import it.
Creating Your First Module
Create a file called models.py:
"""SmartNotes data models."""
from dataclasses import dataclass, field
@dataclass
class Note:
title: str
body: str
word_count: int
author: str = "Anonymous"
is_draft: bool = True
tags: list[str] = field(default_factory=list)
Now create a file called main.py in the same directory:
from models import Note
note = Note(
title="Python Tips",
body="Learn the basics.",
word_count=3,
author="James",
tags=["python"],
)
print(note)
print(f"Title: {note.title}")
print(f"Tags: {note.tags}")
Run it:
uv run python main.py
Output:
Note(title='Python Tips', body='Learn the basics.', word_count=3, author='James', is_draft=True, tags=['python'])
Title: Python Tips
Tags: ['python']
from models import Note tells Python: "Open models.py, find the class named Note, and make it available here." The Note class lives in one file but is usable everywhere.
Two Ways to Import
Python offers two import styles:
# Style 1: import the module
import models
note = models.Note("Tips", "Learn", 3)
# Style 2: import specific names from the module
from models import Note
note = Note("Tips", "Learn", 3)
| Style | Syntax | When to use |
|---|---|---|
import models | Access via models.Note | When you use many things from the module or names might conflict |
from models import Note | Access via Note directly | When you need just one or two specific names |
Both do the same thing. The difference is how you refer to the imported name in your code. Most Python projects prefer from ... import for specific names.
Splitting SmartNotes: Before and After
Before (everything in one file):
smartnotes_search.py (85 lines)
├── Note class (15 lines)
├── search_notes function (20 lines)
├── filter_by_tag function (5 lines)
├── sort_by_word_count function (5 lines)
└── ... more functions
After (split into focused modules):
models.py -- Note class only (15 lines)
search.py -- search_notes, filter_by_tag (30 lines)
file_manager.py -- FileManager class (90 lines)
main.py -- entry point, uses all modules
Each file has one job:
| Module | Responsibility |
|---|---|
models.py | Data structures (what a Note is) |
search.py | Search and filter operations (finding notes) |
file_manager.py | File I/O (saving and loading notes) |
main.py | Wiring everything together |
Moving Search Functions to Their Own Module
Create search.py:
"""SmartNotes search and filter operations."""
from 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.
- Matches keyword against title and body (case-insensitive)
- If tag is provided, only returns notes that have that tag
- Title matches come before body-only matches
- Empty keyword returns all notes (filtered by tag if provided)
"""
...
The function imports Note from models.py. It does not define Note itself. If you change the Note class in models.py, every module that imports it gets the update automatically. No more duplicated definitions.
Update main.py to use both modules:
from models import Note
from search import search_notes
notes = [
Note("Python Tips", "Learn basics", 2, "James", tags=["python"]),
Note("Debugging", "Fix errors", 2, "James", tags=["debug"]),
Note("Cooking", "Boil water", 2, "Emma", tags=["cooking"]),
]
results = search_notes(notes, "python")
for note in results:
print(f"Found: {note.title}")
Output:
Found: Python Tips
Two imports, two modules, one program. Each module is focused, testable, and reusable.
What Happens When You Import
When Python encounters from models import Note, it does three things:
- Finds
models.pyin the current directory (or the Python path) - Runs all the code in
models.pyfrom top to bottom - Binds the name
Notein your current file
Step 2 is important: Python runs the entire module when you import it. If models.py contains print("Loading models...") at the top level, that print statement runs every time another file imports from models.
# models.py
print("Loading models...")
from dataclasses import dataclass, field
@dataclass
class Note:
title: str
body: str
word_count: int
# main.py
from models import Note # This prints "Loading models..."
Output:
Loading models...
Python runs the import only once per program execution. If main.py and search.py both import from models, the module code runs once (the first time), and subsequent imports reuse the already-loaded module.
PRIMM-AI+ Practice: Predict the Import
Predict [AI-FREE]
Press Shift+Tab to enter Plan Mode.
You have two files:
# helpers.py
def double(x: int) -> int:
return x * 2
def triple(x: int) -> int:
return x * 3
# main.py
from helpers import double
print(double(5))
print(triple(5))
What happens when you run main.py? Does it print two numbers, or does it raise an error? Write your prediction.
Check your prediction
NameError: name 'triple' is not defined
from helpers import double imports only double. The name triple is not available in main.py. You would need from helpers import double, triple or import helpers then helpers.triple(5).
Run
Press Shift+Tab to exit Plan Mode.
Create both files and run uv run python main.py. Verify the error matches your prediction. Then fix it by importing triple as well.
Investigate
Write one sentence explaining why triple(5) failed in the Predict exercise. What would you need to change in the import to make it work?
If you want to go deeper, run /investigate @helpers.py in Claude Code and ask: "What is the difference between import helpers and from helpers import double? When is each one better?"
Modify
Create a third module called math_utils.py that imports double from helpers and uses it to build quadruple(x: int) -> int (which calls double(double(x))). Import quadruple into main.py and test it.
Make [Mastery Gate]
Split your SmartNotes project into three modules: models.py, search.py, and main.py. Move the Note class to models.py, move search_notes to search.py, and have main.py import from both. Verify with:
uv run pyright models.py search.py main.py
uv run pytest test_smartnotes_search.py -v
All type checks and tests must pass after the split.
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: Plan the Module Split
Here are the functions and classes in my SmartNotes project:
- Note (dataclass)
- search_notes, filter_by_tag, sort_by_word_count
- FileManager (class with save/load for JSON, CSV, Markdown)
- export_note_as_markdown, read_note_from_markdown
Suggest a module structure. Which functions go in which
file? Explain why you grouped them that way.
What you're learning: Module design is about grouping by responsibility. The AI suggests a structure and explains the reasoning. You evaluate whether the grouping makes sense for your project.
Prompt 2: Test Import Isolation
After splitting my code into modules, how do I make sure
the imports are correct? Show me a quick test I can run
to verify that every module imports cleanly without
circular dependencies.
What you're learning: Import verification is a real engineering concern. The AI shows you techniques like importing each module in isolation to catch problems early.
Prompt 3: When to Split
My models.py has one class (Note). My search.py has
three functions. Is this too granular? When is it
better to keep things in one file versus splitting
into many small files?
What you're learning: Over-splitting is as bad as under-splitting. The AI helps you find the balance between "one giant file" and "fifty tiny files." The answer depends on project size and team conventions.
James runs pyright across all three modules. Zero errors. He runs the tests. All green. The Note class lives in one place, imported everywhere.
"At the warehouse," he says, "we had a central parts catalog. Every department used the same catalog. If a part number changed, you updated the catalog once and every department had the correct number. Before the central catalog, each department kept its own list. Part numbers drifted. Orders got mixed up."
"That is exactly the problem modules solve," Emma says. "Your models.py is the central catalog. One definition of Note, shared everywhere."
"But right now the modules are all in one directory," James says. "What happens when I have ten modules? Twenty?"
"That is a package. A directory with an __init__.py file that groups related modules together. That is Lesson 2."