Skip to main content

Why Modules?

If you're new to programming

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.

If you've coded before

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)
StyleSyntaxWhen to use
import modelsAccess via models.NoteWhen you use many things from the module or names might conflict
from models import NoteAccess via Note directlyWhen 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:

ModuleResponsibility
models.pyData structures (what a Note is)
search.pySearch and filter operations (finding notes)
file_manager.pyFile I/O (saving and loading notes)
main.pyWiring 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:

  1. Finds models.py in the current directory (or the Python path)
  2. Runs all the code in models.py from top to bottom
  3. Binds the name Note in 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

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