From individual tools to complete programs — requirements, program flow, menus, data design, persistence, testing, and documentation
You have spent the last several chapters building skill with individual tools: classes, objects, methods, attributes, file input and output, exception handling, functions, and data structures. You can write a function that returns a value. You can define a class that holds data and provides methods. You can open a file and read records from it.
Now it is time to combine those tools into something larger. This chapter is about application design — the process of thinking through what a program needs to do before writing most of its code, organizing the pieces so they work together cleanly, and building toward a working program step by step rather than all at once.
A small console application is a complete program that a real user can run, interact with through text menus, and use to accomplish something useful. It might manage a list of contacts. It might track a personal budget. It might keep a log of exercise sessions. The exact purpose is less important than the process: you learn to plan, design, build, test, and document a program that holds together. This chapter prepares you for Chapter 30, where you will build your own capstone project from start to finish.
Every earlier chapter gave you one or two tools to practice in isolation. This chapter teaches you how to assemble those tools into a program that does something real. The contact manager built across this chapter is not a toy — it is a working application you could actually use. Study it as a complete design, not just as a collection of code snippets.
By the end of this chapter, you should be able to:
Throughout this book, most programs you wrote were exercises. An exercise is designed to practice one skill at a time. It is short, focused, and complete when it produces the right output. Exercises are essential. They build the fluency you need to write larger programs.
But applications are different from exercises in several important ways.
None of this means applications are beyond you. It means they require a different working style: a little more planning, a little more structure, a little more patience before writing code. The good news is that the skills transfer directly. Every application is built from the same loops, functions, classes, files, and data structures you have already studied. Designing an application is learning to arrange those pieces well.
In exercises, you wrote code to satisfy a specification given to you. In applications, you must first figure out the specification yourself — by writing requirements. The rest of application design flows from that step.
A requirement is a statement of what a program must do. Requirements are written before code. They describe the program from the outside, from the user's perspective, without specifying how the program works internally. Good requirements answer three questions: What can the user do with this program? What information does the program manage? What should happen in unusual situations?
Consider a simple contact manager. A beginner might describe it as: "A program that stores contacts." That is a direction, not a requirement. Here is a more useful set:
These requirements are specific enough to guide design. They list features, describe the information involved, and mention edge cases. They do not say anything about which functions to write or how the menu looks. Those are implementation decisions, not requirements.
Writing requirements before coding has several benefits: you discover missing features before you have written code around wrong assumptions; you have a checklist for testing; and if you are working with an instructor or collaborator, requirements give you a shared understanding of what you are building.
| Mistake | Poor Example | Better Version |
|---|---|---|
| Too vague | "The program should work well." | "The program must display an error message if the user enters a name with no characters." |
| Mixing in implementation | "The program should use a list of dictionaries." | "The program must store contacts with a name, phone, and email field." (How is your choice.) |
| Forgetting error cases | Requirements cover only the happy path. | Add explicit requirements for bad input, missing files, and empty search results. |
A user story is a short sentence that describes a feature from the user's point of view. The format is:
As a [user], I want to [do something] so that [I get some benefit].
For a beginner project, you can use a simplified version and drop the "so that" clause if the benefit is obvious.
User stories help you think about features from the outside rather than from the implementation. When you ask "what does the user want?" you stay focused on the actual purpose of the program. Here are user stories for a simple budget tracker:
Each story maps to one or more requirements. The first story becomes: "The program must allow the user to add an expense with a text description and a dollar amount. The amount must be a positive number." User stories are most useful when you are not sure what features to include. If you already have a clear picture, you can move directly to requirements.
Program flow is the sequence of steps a running program goes through. For a console application, the flow almost always follows the same pattern: load data, show menu, get choice, perform action, repeat until quit. This is called the application loop.
Before writing code, sketch the flow in plain text. Here is a sketch for the contact manager:
Start
Load contacts from file (or start with empty list if no file)
Loop:
Display menu:
1. Add contact
2. View all contacts
3. Search for contact
4. Delete contact
5. Quit
Get user choice
If choice is 1: collect name, phone, email -> add to contacts list
If choice is 2: display all contacts sorted by name
If choice is 3: ask for name -> search and display result
If choice is 4: ask for name -> remove from contacts list
If choice is 5: save contacts to file -> exit loop
End
This sketch is not code yet. It is a plan. Reading it against your requirements reveals decisions you still need to make: What data structure holds contacts? What file format? How do you sort alphabetically? What happens for an invalid menu choice? Answering these questions during design, rather than during coding, makes the coding phase much smoother.
Translating the sketch to Python produces a skeleton — short but complete in structure:
def main():
contacts = load_contacts()
running = True
while running:
print_menu()
choice = input("Enter your choice: ").strip()
if choice == "1":
add_contact(contacts)
elif choice == "2":
view_contacts(contacts)
elif choice == "3":
search_contacts(contacts)
elif choice == "4":
delete_contact(contacts)
elif choice == "5":
save_contacts(contacts)
print("Contacts saved. Goodbye.")
running = False
else:
print("Invalid choice. Please enter 1 through 5.")
main()
The main() function manages flow and delegates to named functions. It does not implement any feature itself. If main() contains all the logic for every feature, it becomes long, hard to read, and hard to test. Delegating to named functions keeps each feature isolated and testable.
A menu is how a console application communicates its options and collects the user's choice. A good menu is clear, consistent, and forgiving of small mistakes.
def print_menu():
print()
print("--- Contact Manager ---")
print("1. Add contact")
print("2. View all contacts")
print("3. Search for contact")
print("4. Delete contact")
print("5. Quit")
print()
Putting the menu in a function means you call it at the top of every loop iteration without repeating the print statements. Blank lines before and after give breathing room in the terminal, especially after a long list of contacts.
Always validate the user's menu choice. A user might type a letter, a space, or a number that is not on the menu:
def get_menu_choice(valid_choices):
"""Ask for a choice until the user enters a valid one."""
while True:
choice = input("Enter your choice: ").strip()
if choice in valid_choices:
return choice
print(f"Please enter one of: {', '.join(valid_choices)}")
Each feature has its own input-collection step. Notice what add_contact() does and does not do — it collects and validates input, builds the dictionary, appends it, and prints a confirmation. It does not print the menu and does not save the file. Each function has exactly one job:
def add_contact(contacts):
"""Collect contact information and add it to the list."""
print("\n--- Add Contact ---")
name = input("Name: ").strip()
if not name:
print("Name cannot be empty. Contact not added.")
return
phone = input("Phone: ").strip()
email = input("Email: ").strip()
contact = {"name": name, "phone": phone, "email": email}
contacts.append(contact)
print(f"Contact '{name}' added.")
After adding a contact, print a confirmation. After a search finds nothing, say so. After deleting, confirm the deletion. Silence leaves users wondering whether their action worked. Every feature function should end with a message to the user, whether the operation succeeded or failed.
Before writing any feature code, decide how your program will represent its data. A poor data structure can make every operation awkward. A good one makes most operations natural. Consider three representations for contacts:
| Option | What It Looks Like | Best When |
|---|---|---|
| List of dictionaries | [{"name": "Alice", "phone": "555-1234", "email": "…"}, …] |
Data is flat. Operations are mostly iteration. CSV saving is natural. |
| List of objects | [Contact("Alice", "555-1234", "…"), …] |
The data has meaningful behavior — display methods, validation, formatting. |
| Dictionary keyed by name | {"Alice": {"phone": "555-1234", …}, …} |
Lookup by name must be very fast and names are guaranteed unique. |
For this chapter's contact manager, a list of dictionaries (or a list of objects) is the right choice — simple, flexible, easy to iterate, and maps naturally to CSV rows. Use the simplest structure that satisfies your requirements. You can always refactor to a more complex structure later if you need it.
A clean pattern for small applications is: use a class to represent a single piece of data (one contact, one expense, one task), and use standalone functions for operations on collections of that data (add to list, search list, display list). This keeps each class focused on one thing and keeps the application logic in functions that can be called from main().
class Contact:
"""Represents a single contact with name, phone, and email."""
def __init__(self, name, phone, email):
self.name = name
self.phone = phone
self.email = email
def display(self):
"""Print this contact's information."""
print(f" Name: {self.name}")
print(f" Phone: {self.phone}")
print(f" Email: {self.email}")
def to_dict(self):
"""Return this contact as a dictionary for saving."""
return {"name": self.name, "phone": self.phone, "email": self.email}
The display() and to_dict() methods are behaviors specific to a contact, so a class is appropriate. Every function that shows a contact just calls c.display() rather than repeating print statements. Every function that saves contacts just calls c.to_dict().
Do not use classes just to feel like you are writing object-oriented code. If your data has no meaningful behavior beyond __init__, a dictionary is simpler. Classes are most valuable when the data has behavior attached to it.
A program that forgets everything when it closes is not very useful. Most practical applications need persistence: the ability to save data between sessions. For a beginner application, CSV is an excellent choice — easy to read and write with Python's csv module, human-readable in a text editor, and familiar from spreadsheets.
import csv
import os
FILENAME = "contacts.csv"
FIELDNAMES = ["name", "phone", "email"]
def save_contacts(contacts):
"""Save all contacts to a CSV file."""
with open(FILENAME, "w", newline="") as file:
writer = csv.DictWriter(file, fieldnames=FIELDNAMES)
writer.writeheader()
for c in contacts:
writer.writerow(c.to_dict())
print(f"Contacts saved to {FILENAME}.")
def load_contacts():
"""Load contacts from CSV file. Return empty list if file does not exist."""
contacts = []
if not os.path.exists(FILENAME):
return contacts
with open(FILENAME, "r", newline="") as file:
reader = csv.DictReader(file)
for row in reader:
contacts.append(Contact(row["name"], row["phone"], row["email"]))
return contacts
save_contacts() opens in write mode ("w"), overwriting the file — correct, because you are saving the complete current state. load_contacts() uses os.path.exists() before opening, handling the first-run case without crashing. The newline="" argument is required by Python's csv module to prevent extra blank lines on Windows.
| Strategy | How It Works | Best For |
|---|---|---|
| Save on quit | Call save_contacts() only when the user selects Quit. | Simple programs. Acceptable risk of losing data if the program crashes. |
| Save after every change | Call save_contacts() after every add, edit, or delete. | Programs where data loss is unacceptable. Slightly slower but much safer. |
For a beginner project, saving on quit is fine. JSON is a useful alternative format — especially when your data has nested structures that do not fit neatly into rows and columns — but CSV is simpler and human-readable for flat records like contacts.
Testing a complete application requires more effort than testing a single function, because an application has many paths through it. The most reliable approach for a beginner application is a manual testing checklist based on your requirements. Work through every item deliberately.
Work through every item. When something fails, fix it and recheck that item and any related items. Test one feature at a time during development, not only at the end. If you just wrote add_contact(), test it immediately. Catching a bug in a 20-line function is much easier than finding it later in a 200-line program.
Edge cases are inputs at the boundary of normal behavior. You do not have to handle every possible case in a beginner project, but you should think through the most likely ones and decide whether to handle them or to note them as known limitations. For example: "This program does not support contacts with the same name. If two contacts have identical names, both will appear in search results and the first match will be deleted." That is an honest, useful note.
Documentation is writing that helps users and future programmers understand your program. For a small console application, you need two kinds: user documentation (instructions for running and using the program) and code documentation (comments and docstrings that explain how the code works).
A short README file in the same folder as your program is enough. It should answer: What does this program do? How do I run it? What can I do in the program? Where is the data saved? Are there known limitations?
Contact Manager
===============
A simple console application for storing and retrieving contact information.
How to run:
python contact_manager.py
What you can do:
1. Add a new contact (name, phone, email)
2. View all contacts in alphabetical order
3. Search for a contact by name
4. Delete a contact by name
5. Quit (contacts are saved automatically)
Data storage:
Contacts are saved in contacts.csv in the same folder as the program.
Known limitations:
- Two contacts with the same name may cause unexpected behavior when deleting.
- Phone and email are stored as text and are not validated for format.
A docstring is a string literal placed immediately after a function or class definition. Python treats it as the function's documentation. A comment explains why the code does something, not just what it does:
def search_contacts(contacts):
"""
Search for contacts by partial name match.
Parameters:
contacts (list): A list of Contact objects.
Prints all contacts whose names contain the search term.
Returns nothing.
"""
...
# Use newline="" to prevent csv module from adding extra blank lines on Windows
with open(FILENAME, "w", newline="") as file:
...
Good code documentation does not repeat what the code already says clearly. You do not need a comment explaining that contacts.append(new_contact) appends a contact. You do need a comment explaining a non-obvious decision, a workaround, or a limitation. For a beginner project, aim for: a module-level docstring, a docstring on every function and class, and comments on any code that is not immediately obvious.
Here is the complete contact manager, putting all the pieces from sections 28.1–28.10 together. Study it as a complete design before reading the breakdown below.
"""
contact_manager.py
A simple console application for storing and managing contacts.
Contacts are saved to and loaded from contacts.csv.
Usage:
python contact_manager.py
"""
import csv
import os
FILENAME = "contacts.csv"
FIELDNAMES = ["name", "phone", "email"]
# ── Data Model ────────────────────────────────────────────────────────────────
class Contact:
"""Represents a single contact with name, phone, and email."""
def __init__(self, name, phone, email):
self.name = name
self.phone = phone
self.email = email
def display(self):
"""Print this contact's information."""
print(f" Name: {self.name}")
print(f" Phone: {self.phone}")
print(f" Email: {self.email}")
def to_dict(self):
"""Return this contact as a dictionary (for CSV saving)."""
return {"name": self.name, "phone": self.phone, "email": self.email}
# ── File Operations ───────────────────────────────────────────────────────────
def load_contacts():
"""
Load contacts from the CSV file.
Returns an empty list if the file does not exist.
"""
contacts = []
if not os.path.exists(FILENAME):
return contacts
with open(FILENAME, "r", newline="") as file:
reader = csv.DictReader(file)
for row in reader:
contacts.append(Contact(row["name"], row["phone"], row["email"]))
return contacts
def save_contacts(contacts):
"""Save all contacts to the CSV file, overwriting previous data."""
with open(FILENAME, "w", newline="") as file:
writer = csv.DictWriter(file, fieldnames=FIELDNAMES)
writer.writeheader()
for c in contacts:
writer.writerow(c.to_dict())
# ── Feature Functions ─────────────────────────────────────────────────────────
def add_contact(contacts):
"""Collect input and add a new Contact to the list."""
print("\n--- Add Contact ---")
name = input("Name: ").strip()
if not name:
print("Name cannot be empty. Contact not added.")
return
phone = input("Phone: ").strip()
email = input("Email: ").strip()
contacts.append(Contact(name, phone, email))
print(f"Contact '{name}' added.")
def view_contacts(contacts):
"""Display all contacts sorted alphabetically by name."""
print("\n--- All Contacts ---")
if not contacts:
print("No contacts stored yet.")
return
sorted_contacts = sorted(contacts, key=lambda c: c.name.lower())
for c in sorted_contacts:
c.display()
print()
def search_contacts(contacts):
"""Search for contacts by partial, case-insensitive name match."""
print("\n--- Search Contacts ---")
term = input("Search name: ").strip().lower()
if not term:
print("No search term entered.")
return
results = [c for c in contacts if term in c.name.lower()]
if not results:
print("No contacts found matching that name.")
else:
print(f"{len(results)} result(s):")
for c in results:
c.display()
print()
def delete_contact(contacts):
"""Remove the first contact whose name exactly matches the input."""
print("\n--- Delete Contact ---")
name = input("Enter exact name to delete: ").strip()
for i, c in enumerate(contacts):
if c.name.lower() == name.lower():
contacts.pop(i)
print(f"Contact '{c.name}' deleted.")
return
print(f"No contact named '{name}' found.")
# ── Menu ──────────────────────────────────────────────────────────────────────
def print_menu():
"""Display the main menu."""
print()
print("=== Contact Manager ===")
print("1. Add contact")
print("2. View all contacts")
print("3. Search for contact")
print("4. Delete contact")
print("5. Quit")
print()
def get_choice():
"""Ask for a menu choice until the user enters 1 through 5."""
while True:
choice = input("Your choice: ").strip()
if choice in ("1", "2", "3", "4", "5"):
return choice
print("Please enter 1, 2, 3, 4, or 5.")
# ── Main ──────────────────────────────────────────────────────────────────────
def main():
"""Run the contact manager application."""
contacts = load_contacts()
print(f"Loaded {len(contacts)} contact(s).")
running = True
while running:
print_menu()
choice = get_choice()
if choice == "1":
add_contact(contacts)
elif choice == "2":
view_contacts(contacts)
elif choice == "3":
search_contacts(contacts)
elif choice == "4":
delete_contact(contacts)
elif choice == "5":
save_contacts(contacts)
print("Contacts saved. Goodbye.")
running = False
main()
main() is a sign of good design.add_contact() adds. save_contacts() saves. get_choice() validates input. No function tries to do too much.load_contacts() checks for file existence before opening — no try/except needed, because a missing file on first run is expected, not exceptional.Read the following task manager program carefully. Answer the questions below without running it.
TASKS_FILE = "tasks.txt"
def load_tasks():
tasks = []
try:
with open(TASKS_FILE, "r") as file:
for line in file:
line = line.strip()
if line:
tasks.append(line)
except FileNotFoundError:
pass
return tasks
def save_tasks(tasks):
with open(TASKS_FILE, "w") as file:
for task in tasks:
file.write(task + "\n")
def add_task(tasks):
description = input("Task description: ").strip()
if description:
tasks.append(description)
print("Task added.")
else:
print("No description entered.")
def view_tasks(tasks):
if not tasks:
print("No tasks.")
return
for i, task in enumerate(tasks, start=1):
print(f"{i}. {task}")
def complete_task(tasks):
view_tasks(tasks)
if not tasks:
return
try:
number = int(input("Complete which task number? "))
if 1 <= number <= len(tasks):
done = tasks.pop(number - 1)
print(f"Completed: {done}")
else:
print("Invalid number.")
except ValueError:
print("Please enter a number.")
def main():
tasks = load_tasks()
running = True
while running:
print("\n1. Add 2. View 3. Complete 4. Quit")
choice = input("Choice: ").strip()
if choice == "1":
add_task(tasks)
elif choice == "2":
view_tasks(tasks)
elif choice == "3":
complete_task(tasks)
elif choice == "4":
save_tasks(tasks)
print("Saved. Goodbye.")
running = False
else:
print("Invalid choice.")
main()
tasks.txt does not exist when the program starts. Does the program crash or continue normally? Why is this approach different from the contact manager's approach with os.path.exists()?enumerate(tasks, start=1) do that enumerate(tasks) alone would not? Why does this matter for the user?complete_task(), what would happen if the user enters 0? What would happen if they enter a number larger than the length of the list?main() is on one line: "1. Add 2. View 3. Complete 4. Quit". What are the tradeoffs of this compact format compared to the multi-line menu in the contact manager?Trace through the following sequence of calls against the contact manager from the worked example. For each step, write the state of the contacts list, what was printed, and whether the operation succeeded or was rejected.
contacts = []
add_contact(contacts)
# User inputs: name="Priya Mehta", phone="555-9001", email="[email protected]"
add_contact(contacts)
# User inputs: name="" (empty), phone=(not reached), email=(not reached)
add_contact(contacts)
# User inputs: name="Omar Diallo", phone="555-4321", email="[email protected]"
delete_contact(contacts)
# User inputs: name="priya mehta" (lowercase)
Assume contacts is a list of three Contact objects with names "Zara Kim", "Alice Chen", and "Ben Okafor". What will this code print?
sorted_contacts = sorted(contacts, key=lambda c: c.name.lower())
for c in sorted_contacts:
print(c.name)
Write the names in the order they will be printed. Then explain why c.name.lower() is used as the sort key rather than c.name alone.
Each program below has a bug. Identify the bug, explain why it causes a problem, and write the corrected code.
def delete_contact(contacts):
name = input("Name to delete: ").strip()
for c in contacts:
if c.name.lower() == name.lower():
contacts.remove(c)
print(f"Deleted {c.name}.")
enumerate) and return as soon as the first match is found, or to collect indices separately before modifying.def load_contacts():
contacts = []
with open("contacts.csv", "r", newline="") as file:
reader = csv.DictReader(file)
for row in reader:
contacts.append(Contact(row["name"], row["phone"], row["email"]))
return contacts
contacts.csv does not exist?FileNotFoundError on first run when no file exists yet. Add either an os.path.exists() check before opening or a try/except FileNotFoundError block that returns an empty list.def add_contact(contacts):
name = input("Name: ").strip()
phone = input("Phone: ").strip()
email = input("Email: ").strip()
if not name:
print("Name cannot be empty.")
contacts.append(Contact(name, phone, email))
print("Contact added.")
contacts.append() call should be inside an else branch, or the function should return immediately after printing the error.For each application below, write: (1) a list of 5–8 requirements; (2) a sketch of the program flow; (3) the data structure you would use and why.
For each scenario, write the input you would use, the action you would take, and the expected result.
view_contacts() function sorts contacts by name. Modify it to print a numbered list (1. Alice Chen, 2. Ben Okafor, etc.) instead of just the contact details.load_tasks() and save_tasks() so each line is formatted as "PENDING|Buy groceries" or "DONE|Call the dentist", and the program tracks whether each task is pending or done.count_contacts(contacts) function that prints how many contacts are stored and how many have non-empty email addresses.delete_contact(). After the contact is found, print "Delete [name]? (y/n): " and only delete if the user confirms with y.main() short. Validate all user input — the program must never crash on bad input.| Application | What It Manages | Key Features |
|---|---|---|
| Personal book log | Books read: title, author, rating | Add, view by rating, search by title |
| Exercise log | Sessions: date, activity, duration | Add, view all, total time per activity |
| Recipe collection | Recipes: name, ingredients, instructions | Add, search by name, view details |
| Vocabulary study tool | Words and definitions | Add, quiz mode, view all |
| Shopping list manager | Items with quantity and category | Add, mark as bought, view remaining |
main() function is short and delegates all feature work to named functions.while loop in a console application that repeatedly shows a menu, collects input, and performs actions until the user quits.main() function that uses it, using pseudocode placeholders for features.main() and feature functions. What should main() do, and what should it delegate?load_contacts() handle the case where the file does not exist? What would happen without that check?main() is 80 lines long and contains all the logic for every feature. What problems does this cause? How should the code be reorganized?view_contacts(): if len(contacts) == 0 and if not contacts. Which is more Pythonic? Are they equivalent?delete_contact() function uses contacts.pop(i) after finding the index with enumerate. Why is this approach safer than calling contacts.remove(c) inside a for c in contacts loop?budget_tracker.py application. Write: (a) five requirements, (b) a program flow sketch, (c) the data structure you would use for expenses and why, (d) the name and purpose of each function you would write.search_contacts() uses partial, case-insensitive matching. Describe a situation where this is the right behavior and a situation where exact matching would be better. How would you change the function for exact matching?main() function that implements the application loop pattern and stays short.save_contacts() and load_contacts() using the csv module.Chapter 29 turns the focus from building new applications to improving existing ones. You will learn what code quality means: readable names, clear structure, useful comments, and code that is easy to change. You will practice refactoring — rewriting working code to make it cleaner without changing what it does. You will write your first unit tests and assertions to verify that individual functions behave correctly. And you will apply a code review checklist to the application you built in this chapter. These skills will prepare you for the capstone project in Chapter 30.