Building a Todo Console App (Capstone Project)
The Challenge: You manage dozens of tasks across work, personal projects, and learning. Your todo list is scattered across sticky notes, apps, and your head. You need a single source of truth—but building a full web application feels like overkill.
What if you could build a command-line todo manager that lets you add, complete, and organize tasks from the terminal? No database required. Just clean Python functions that do one thing well. This capstone teaches you exactly that.
By the end, you'll have a working multi-module todo console app that demonstrates professional code organization. And more importantly, you'll understand why this structure matters: functions that are easy to test, modules that are easy to reuse, and a main program that's easy to understand.
Capstone Project Overview
The Task: Build a multi-module todo application that demonstrates:
- Separation of concerns: Task operations live separately from I/O and orchestration
- Function-based architecture: Each todo operation (add, complete, list, filter) is a testable function
- Canonical task structure: Tasks use a consistent dictionary format across the entire app
- Type safety: Functions use type hints and return Optional values for edge cases
- Multi-module imports: Main program imports and coordinates specialized modules
- Comprehensive testing: Validate that functions work correctly with various inputs
Project Structure:
todo_app/
├── task_operations.py # Task manipulation functions
├── utils.py # I/O and display formatting
├── main.py # Main program orchestrating the app
└── test_todo.py # Tests for task operations
The Transition: You've learned functions in isolation. This capstone shows how functions work together to build something real. You'll experience why good design (modules with clear purposes) makes code easier to test, modify, and extend.
Understanding Task Structure
Before you code, understand how tasks will be represented. A task is a dictionary with these fields:
Loading Python environment...
Note: This matches the canonical Task entity used throughout Part 5. When you move to Chapter 27, you'll wrap this in a Task class. For now, dictionaries are simpler and let you focus on function design.
Organizing Task Operations
When you're building a todo app:
"I'm designing a todo application. Should I put task operations (add, complete, list) in the same module as user interface (display, input)? Or should I separate them?"
AI Response: "Separation makes sense here. Task operations are pure functions—they take tasks and return modified tasks. UI is different—it handles printing, input validation, menu display. If you keep them separate, you can:
- Test operations without simulating user input
- Reuse operations in different UIs (web, mobile, terminal)
- Change how the display works without touching task logic"
Your insight: "So I'd have task_operations.py for the logic and utils.py for the interface?"
AI: "Exactly. Plus main.py orchestrates them both. And test_todo.py validates operations without touching the UI."
This conversation shows real architecture thinking. Don't just follow the structure blindly—understand why separation helps.
Step 1: Create task_operations.py — Task Manipulation Functions
This module contains pure functions that create, modify, and manage tasks. No printing, no input—just data transformation.
💻 Code Idea: Task Operations Module
Loading Python environment...
Output:
# No output yet—these are just function definitions.
# They'll be used by main.py and tested by test_todo.py
Design Choices:
- All functions are pure (no printing, no input)
- Functions take
tasks: listas parameter (modifying in place for efficiency) - Return types are explicit:
boolfor success/failure,dict | Nonefor optional results - Edge cases handled gracefully (task not found → False, validation errors → ValueError)
- Global
_next_idsimulates database auto-incrementing (temporary—Chapter 25 will use JSON files)
Step 2: Create utils.py — User Interface and Display
This module handles everything the user sees and inputs. It orchestrates task_operations but doesn't modify tasks directly.
💻 Code Idea: Utilities Module
Loading Python environment...
Output:
# No output yet—these are display functions called by main.py
Design Choices:
- Input functions return tuples or None (not direct task dictionaries)
- Display functions take already-created data (from task_operations) and format it
- Input validation handles numbers, empty strings, and invalid ranges
- All user-facing feedback is centralized here (easier to change UI style globally)
Step 3: Create main.py — Main Program
This file orchestrates the app by importing and coordinating the other modules.
💻 Code Idea: Main Program
Loading Python environment...
Output (when run):
========================================
TODO CONSOLE APP
========================================
1. Add a new task
2. List all tasks
3. Mark task complete
4. List pending tasks
5. List completed tasks
6. Filter by priority
7. Update task priority
8. Exit
========================================
Enter choice (1-8): 1
Task title: Review PR
Description (optional): Check code quality
Priority (1=highest, 10=lowest, default 5): 2
✨ Added task #1: Review PR
...
Key Patterns:
import task_operations as ops: Imports custom moduleimport utils: Imports second custom module- Function calls like
ops.add_task()andutils.display_tasks()show module.function pattern - Logic is clear: display menu, get input, call operation, display result
- Each choice orchestrates multiple modules
Step 4: Create test_todo.py — Comprehensive Testing
This file validates that functions in task_operations.py work correctly for various inputs.
💻 Code Idea: Test Module
Loading Python environment...
Output:
✓ test_create_task PASSED
✓ test_create_task_validation PASSED
✓ test_add_task PASSED
✓ test_complete_task PASSED
✓ test_list_tasks PASSED
✓ test_filter_tasks_by_status PASSED
✓ test_filter_tasks_by_priority PASSED
✓ test_filter_tasks_combined PASSED
✓ test_get_task_by_id PASSED
✓ test_update_task_priority PASSED
✓ All tests passed!
Testing Patterns:
reset_id_counter(): Ensures deterministic IDs across test runs- Each test function checks one operation or edge case
- Use
assertstatements to verify expected behavior - Test normal cases, edge cases, and error cases
- Catch exceptions with try/except to test error handling
- Run all tests to validate the entire project
How to Run the Project
Run the Todo App (Interactive)
# Navigate to project directory
cd todo_app/
# Run the main program
python main.py
The app will display a menu. Choose an option (1-8) and follow prompts.
Example Session:
========================================
TODO CONSOLE APP
========================================
1. Add a new task
2. List all tasks
3. Mark task complete
4. List pending tasks
5. List completed tasks
6. Filter by priority
7. Update task priority
8. Exit
========================================
Enter choice (1-8): 1
Task title: Review PR
Description (optional): Check for code quality
Priority (1=highest, 10=lowest, default 5): 2
✨ Added task #1: Review PR
Run the Tests (Validation)
# Run tests to verify operations work
python test_todo.py
# Output should show:
# ✓ test_create_task PASSED
# ✓ test_add_task PASSED
# ... (all tests)
# ✓ All tests passed!
Understanding the Project Structure
Why Separate Files?
task_operations.py — Pure Task Functions
- Does: Manipulate task data (add, complete, filter, update)
- Doesn't: Print, read input, modify global state (except for ID counter)
- Benefit: Easy to test in isolation, easy to reuse with different UIs (web, mobile, etc.)
utils.py — User Interface
- Does: User interaction, input validation, display formatting
- Doesn't: Manipulate task data directly (calls task_operations for that)
- Benefit: Can be reused for other programs that need similar input/output patterns
main.py — Orchestration
- Does: Import and coordinate other modules, implement application flow
- Doesn't: Do task manipulation or complex I/O directly
- Benefit: Clear, readable flow of program logic—shows how modules work together
test_todo.py — Verification
- Does: Test task_operations functions with various inputs
- Benefit: Confidence that functions work before using them in the UI
Example: Why This Separation Matters
Imagine you want to add a web interface. Here's what happens:
Without separation (calculator.py):
Loading Python environment...
With separation (our todo app):
Loading Python environment...
This is why separation of concerns scales.
Module Imports — How It Works
Loading Python environment...
Python searches for modules in this order:
- Same directory as main.py ✓ (where our modules are)
- Standard library (
json,math, etc.) - Installed packages (
requests,numpy, etc.)
Since all files are in the same todo_app/ directory, imports work automatically.
Understanding the Task Data Structure
Every task is a dictionary with consistent structure:
Loading Python environment...
Important: All functions expect tasks in this format. This consistency is what makes the app work:
- Display functions know which fields to show
- Filter functions can reliably check "done" and "priority"
- Tests can verify all fields are present and correct
When you move to Chapter 27, you'll replace this dictionary with a Task class—but the structure remains the same.
Extend Your Todo App
Your todo app is working! Now extend it:
-
Add a new operation to
task_operations.py— for example:delete_task(tasks, task_id)— Remove a task completelyupdate_description(tasks, task_id, new_description)— Edit a task's descriptionget_high_priority_tasks(tasks)— Return tasks with priority 1-3
-
Add the operation to
main.py— add it to the menu and create a branch for it -
Add tests for your new operation to
test_todo.py
Example: Implementing delete_task
Loading Python environment...
Try With AI
Build a complete multi-module todo console app integrating all Chapter 23 concepts.
🏗️ Explore Architecture Decisions:
"I want to build a todo app with task operations, a user interface, and tests. Should task operations (add, complete, filter) be in the same module as user interface code (displaying menus, getting input)? What are the pros and cons?"
🎯 Practice Task Operations Implementation:
"Implement task_operations.py with these functions: create_task (validates title, priority 1-10), add_task (appends to list), complete_task (marks done=True, status='completed'), filter_tasks (by done status and/or priority). Use type hints like
def complete_task(tasks: list, task_id: int) -> boolfor operations that may fail. Include docstrings for each function."
🧪 Test Multi-Module Behavior:
"Write comprehensive tests for task_operations.py covering: normal cases (add task, mark complete), edge cases (task doesn't exist, invalid priority), error cases (empty title). For each operation, verify the task list is modified correctly and return values match expectations."
🚀 Integrate Everything:
"Build working main.py that imports task_operations and utils. Implement a menu system (1-8 choices): Add, List All, Mark Complete, List Pending, List Completed, Filter by Priority, Update Priority, Exit. For each choice, get input using utils functions, call task_operations functions, and display results using utils display functions. Make sure to handle invalid inputs gracefully."