SmartNotes Package Capstone
This is a timed refactoring challenge. Refactoring means reorganizing code without changing what it does. You move existing SmartNotes code into a proper package structure. No new features, just better organization. The key constraint: all existing tests must still pass after the reorganization. The <details> blocks have hints if you get stuck.
25 minutes. Refactor SmartNotes from flat files into a package. Design the module architecture, write the __init__.py public API, create the __main__.py entry point, and verify everything with pyright and pytest.
Emma looks at the SmartNotes project directory. Files everywhere: Note class, search functions, FileManager, tests, scripts. "You have the code. You have the tests. Now organize it into something a colleague could navigate without asking you where things are."
She sets the timer. "Twenty-five minutes. The tests are your safety net. If they pass after the refactoring, you did not break anything."
The Problem
Reorganize your SmartNotes code into this package structure:
project/
├── smartnotes/
│ ├── __init__.py # Public API
│ ├── __main__.py # Entry point
│ ├── models.py # Note dataclass
│ ├── search.py # search_notes, filter_by_tag, sort_by_word_count
│ └── storage.py # FileManager class
├── tests/
│ ├── test_models.py # Tests for Note
│ ├── test_search.py # Tests for search functions
│ └── test_storage.py # Tests for FileManager
└── pyproject.toml # (optional) package metadata
Your deliverables:
| Deliverable | Verification |
|---|---|
smartnotes/ package with 5 files | ls smartnotes/ shows all files |
__init__.py exports Note, search_notes, FileManager | python -c "from smartnotes import Note, search_notes, FileManager" |
__main__.py runs a demo | python -m smartnotes prints output |
| All tests pass | uv run pytest tests/ -v all GREEN |
| No circular imports | uv run pyright smartnotes/ zero errors |
Start the timer.
Step 1: Design the Architecture (5 minutes)
Before moving any files, draw the dependency graph:
__init__.py → models.py (Note)
→ search.py (search_notes, filter_by_tag)
→ storage.py (FileManager)
search.py → models.py (needs Note type)
storage.py → models.py (needs Note type)
__main__.py → __init__.py (uses public API)
Verify: all arrows point down toward models.py. No cycles.
Decide what is public (exported in __init__.py) and what is internal:
| Public (exported) | Internal (not exported) |
|---|---|
Note | note_from_csv_row |
search_notes | filter_by_tag |
FileManager | sort_by_word_count |
exclude_drafts |
Hint: why are filter functions internal?
filter_by_tag and sort_by_word_count are utility functions used by search_notes and FileManager. External users call search_notes (which uses filters internally). Tests import the internal functions directly from smartnotes.search for unit testing.
Step 2: Create the Package (5 minutes)
Create the directory structure and move your code:
mkdir -p smartnotes tests
Create each file. The bodies should contain the code you have already written in Chapters 57, 58, and 62. If you are starting fresh, write stubs (placeholder functions with ... as the body, like you practiced in the TDG cycle).
smartnotes/models.py: The Note dataclass only.
smartnotes/search.py: Search and filter functions, importing Note from smartnotes.models.
smartnotes/storage.py: FileManager class, importing Note from smartnotes.models.
smartnotes/__init__.py: Re-exports the public API.
"""SmartNotes: a note-taking application with search and file persistence."""
from smartnotes.models import Note
from smartnotes.search import search_notes
from smartnotes.storage import FileManager
__all__ = ["Note", "search_notes", "FileManager"]
smartnotes/__main__.py: A demo that exercises all features.
Run uv run pyright smartnotes/ after creating each file. Fix type errors as they appear.
Step 3: Migrate Tests (5 minutes)
Update your test imports to point to the new package:
Before:
from smartnotes_search import Note, search_notes
from file_manager import FileManager
After:
from smartnotes import Note, search_notes, FileManager
# OR for internal functions:
from smartnotes.search import filter_by_tag
Move test files into the tests/ directory. Run:
uv run pytest tests/ -v
Every test should pass. If any fail, the import paths need fixing, not the logic.
Hint: common test migration issues
- ModuleNotFoundError: The import path changed. Update
from old_module import ...tofrom smartnotes.module import .... - Tests that create files: Tests using
tmp_pathshould still work. Tests using hardcoded paths may need updating. - Missing fixtures: If test helper functions (like
sample_notes()) were defined in the old test files, copy them into the new test files.
Step 4: Verify (3 minutes)
Run the full verification stack:
uv run ruff check smartnotes/
uv run pyright smartnotes/
uv run pytest tests/ -v
uv run python -m smartnotes
All four commands should succeed:
ruff: no lint errorspyright: no type errorspytest: all tests passpython -m smartnotes: demo runs and prints output
Step 5: Review (2 minutes)
Check your package:
- No circular imports: Does
uv run python -c "import smartnotes"work without errors? - Clean public API: Does
from smartnotes import Note, search_notes, FileManagerimport exactly the three names you intended? - Internal access works: Can tests still import
from smartnotes.search import filter_by_tag? - No duplicated code: Is the
Noteclass defined in exactly one place (models.py)?
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:
smartnotes/
├── __init__.py
├── __main__.py
├── models.py
├── search.py
└── storage.py
And here are the imports in each file:
[paste the import section of each file]
Review the structure. Are the dependencies clean?
Any suggestions for improvement?
What you're learning: Package review catches structural issues that individual file reviews miss. The AI evaluates the overall architecture, not just the code in each file.
Prompt 2: Add pyproject.toml
I want to make my SmartNotes package installable with pip.
What do I need in a pyproject.toml file? Show me a minimal
configuration that lets someone run:
pip install .
python -m smartnotes
What you're learning: pyproject.toml is the standard Python package metadata file. This is a preview of how real Python packages are distributed.
Prompt 3: Rate My Refactoring
I refactored SmartNotes from flat files into a package.
Here is what I did:
1. Created smartnotes/ directory with __init__.py
2. Moved Note to models.py
3. Moved search functions to search.py
4. Moved FileManager to storage.py
5. Created __main__.py entry point
6. Updated all test imports
7. All tests pass, pyright clean
Rate my refactoring. What would a senior developer
change?
What you're learning: Self-evaluation after refactoring builds judgment. The AI acts as a code reviewer who evaluates your structural decisions, not just whether the code works.
James runs the full verification stack. All green. He runs python -m smartnotes. The demo prints three notes and a search result. He runs the tests. Twelve tests, all passing.
"I did not write a single new function," he says. "I moved code. Changed imports. Updated tests. The logic is identical."
"That is refactoring," Emma says. "Restructuring code without changing behavior. The tests prove the behavior did not change. If any test broke during the move, you would know exactly where the import went wrong."
She looks at the package structure. "Your SmartNotes has grown from a single function to a package with five modules, three public exports, and a runnable entry point. The TDG cycle works for functions, classes, and now packages. The building blocks scale."
James thinks about the code inside the package. "The search function uses a loop to filter notes. The FileManager uses loops to process files. Everything is loop and if-statement. Is there a more expressive way to write these transformations?"
"Comprehensions," Emma says. "List comprehensions, dict comprehensions, generators. Python's most powerful syntax for transforming data. Chapter 64 teaches them."
"Like batch processing at the warehouse," James says. "Instead of handling one item at a time, you describe what you want and the system processes the whole batch."
"Exactly. And when the batch is too large for memory, generators process it one piece at a time without loading everything at once."