MGU Computer Science Series — Programming with Python
Part VI: Object-Oriented and Larger Programs
Requirements · Design · Menus · Persistence · Testing · Documentation
Chapter 28 of 30
28
MGU Computer Science Series  ·  Programming with Python  ·  Library Textbook Edition

Chapter 28:
Designing Small
Applications

From individual tools to complete programs — requirements, program flow, menus, data design, persistence, testing, and documentation

■ Part VI of VII ■

■ 10 Sections  •  Chapter 28 of 30 ■

Prerequisite: Classes and Objects (Ch. 26–27) · File I/O (Ch. 21–22) · Exceptions (Ch. 23)

Chapter Introduction and Learning Objectives

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.

Chapter Purpose

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.

10Sections
7Objectives
12Key Terms
30Total Chapters

Learning Objectives

By the end of this chapter, you should be able to:

  1. 1Plan a small console application from requirements through working code.
  2. 2Write clear requirements and user stories before writing any code.
  3. 3Sketch program flow and design a main application loop.
  4. 4Build a menu-driven interface with input validation and user feedback.
  5. 5Choose an appropriate data structure and connect functions and classes.
  6. 6Save and load data from a CSV file to give programs persistence.
  7. 7Test a complete application using a manual checklist, and write user documentation and code documentation.

Sections at a Glance

Sections 28.1 – 28.3
Planning: What to Build and Why
Exercises vs. Applications · Requirements · User Stories

From Exercises to Applications

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.

Difference 1
Applications serve a user
An exercise exists to demonstrate that you can write a loop or define a class. An application exists because someone finds it useful. The user's needs shape every decision you make.
Difference 2
Applications run repeatedly
An exercise runs once and finishes. An application runs every day, or whenever the user needs it. It must handle many different inputs and store data between sessions.
Difference 3
Applications have multiple parts
An exercise is usually one function or loop. An application has menus, data structures, file operations, validation, multiple functions, and possibly classes. A design step helps you understand what those parts are before writing.
Difference 4
Applications can fail subtly
If an exercise produces wrong output, you see it immediately. An application can quietly store bad data or miss edge cases. Testing a complete application requires more thought than testing a single function.

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.

The Key Shift

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.

Requirements

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:

Example 28.2.1
Contact Manager Requirements
  • FeaturesThe program must allow the user to: (1) add a new contact with name, phone, and email; (2) view all contacts in alphabetical order by name; (3) search for a contact by name; (4) delete a contact by name; (5) quit the program.
  • PersistenceThe program must save contacts to a file so they persist between sessions. It must load contacts from the file when the program starts. It must handle the case where no file exists yet (first run).
  • Error casesThe program must reject a new contact if the name field is empty. It must inform the user if a search finds no matching contact.

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.

Common Requirement Mistakes

MistakePoor ExampleBetter 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 casesRequirements cover only the happy path.Add explicit requirements for bad input, missing files, and empty search results.

User Stories at an Introductory Level

A user story is a short sentence that describes a feature from the user's point of view. The format is:

User Story Format

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.

Sections 28.4 – 28.7
Design: Flow, Menus, Data, and Code Structure
Application Loop · Menu Validation · Data Structures · Functions vs. Classes

Designing Program Flow

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.

The Main Loop in Python

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()
Keep main() Short

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.

Menus and User Interaction

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.

Displaying a Menu

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.

Collecting and Validating a Choice

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)}")

Collecting Input for a Feature

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.")
Always Give Feedback

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.

Choosing Data Structures

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:

OptionWhat It Looks LikeBest 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.

General Principles
When to reach for each data structure
  • ListWhen order matters or when you need to iterate over all items.
  • DictionaryWhen you need fast lookup by a named key.
  • ClassWhen the data has associated behavior — methods that belong to the item, not to the collection.
  • TupleWhen the data is fixed, positional, and should not change.
  • SetWhen you only care about membership and uniqueness, not order or repetition.

Combining Functions and Classes

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().

When to Avoid Classes

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.

Sections 28.8 – 28.10
Finishing Well: Persistence, Testing, and Documentation
CSV Files · Manual Testing Checklists · README · Docstrings

Saving and Loading Data

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
Three Details Worth Noting

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.

When to Save

StrategyHow It WorksBest For
Save on quitCall save_contacts() only when the user selects Quit.Simple programs. Acceptable risk of losing data if the program crashes.
Save after every changeCall 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 the Application

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.

Example 28.9.1
Contact Manager Manual Testing Checklist
  • SetupRun with no contacts file — does it start without errors? Does the menu display correctly?
  • AddingAdd a contact — does it appear in View All? Try adding with empty name — is it rejected? Add multiple contacts — do all appear?
  • ViewingView all — are contacts sorted alphabetically? View all when empty — does the program say so?
  • SearchingSearch for existing name — correct contact appears? Search partial name — partial matches found? Search missing name — "not found" message?
  • DeletingDelete existing contact — disappears from View All? Delete missing name — handled gracefully?
  • PersistenceAdd contacts, quit, restart — are contacts still there? Delete a contact, quit, restart — is it still gone?
  • MenuEnter invalid choice — does program ask again? Enter letters instead of number — handled?

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.

Thinking About Edge Cases

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 and User Instructions

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).

User Documentation — the README

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.

Code Documentation

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:
    ...
What Goes Where

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.

Worked Example
Complete Contact Manager
Full Source · Structural Breakdown · Design Notes

Complete Contact Manager — Full Source and Breakdown

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()

Design Breakdown

Structure
Four clear sections
Data model, file operations, feature functions, and main. This organization makes it easy to find any part of the program without searching.
main() length
15 lines
Loads data, runs the loop, delegates to feature functions, and stops. A short, readable main() is a sign of good design.
Single responsibility
Each function does one job
add_contact() adds. save_contacts() saves. get_choice() validates input. No function tries to do too much.
User feedback
Every action gets a response
Every feature function either confirms success or explains the outcome. The user always knows what happened.
Missing file
Handled gracefully
load_contacts() checks for file existence before opening — no try/except needed, because a missing file on first run is expected, not exceptional.
Style
Consistent throughout
All functions have docstrings. All names are lowercase with underscores. Indentation is four spaces. The file is easy to read and extend.

Code Reading, Tracing, Debugging, and Practice

ActivitiesCode Reading Practice

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()
  1. What data structure does this program use to store tasks? Why is a list appropriate here?
  2. Trace what happens if 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()?
  3. What does enumerate(tasks, start=1) do that enumerate(tasks) alone would not? Why does this matter for the user?
  4. In 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?
  5. The tasks are saved as plain text lines rather than CSV. What is one advantage and one disadvantage of this format compared to CSV?
  6. The menu in 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?
  7. If you wanted to add a feature to mark a task as "in progress" without removing it, how would you need to change the data structure?
TracingCode Tracing

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)
PredictPredict the Output

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.

DebuggingDebugging Practice

Each program below has a bug. Identify the bug, explain why it causes a problem, and write the corrected code.

Bug 1

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}.")
Hint
This code mostly works but has a subtle problem.
Think about what happens if two contacts have the same name, and whether modifying a list while iterating over it can cause issues. The safer approach is to iterate by index (using enumerate) and return as soon as the first match is found, or to collect indices separately before modifying.

Bug 2

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
Hint
What happens if contacts.csv does not exist?
This function will raise a 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.

Bug 3

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.")
Hint
There is a logic error, not a syntax error.
When the name is empty, the function prints an error message but continues to append the contact anyway. The contacts.append() call should be inside an else branch, or the function should return immediately after printing the error.
DesignApplication Planning Practice

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.

  1. Personal Expense Tracker. The user can add expenses with a description and dollar amount, view all expenses, see the total amount spent, and clear all expenses. Data is saved between sessions.
  2. Vocabulary Flashcard Quiz. The user can add vocabulary words and definitions, take a quiz (the program shows each word in random order and asks for the definition), see their score, and quit. Words are saved between sessions.
  3. Simple Daily Journal. The user can write a new journal entry (stored with today's date), view all entries from newest to oldest, search entries for a keyword, and quit. Entries are saved between sessions.
AnalysisData Design Questions
  1. The contact manager uses a list of Contact objects. Could it instead use a dictionary keyed by name? What would change about the add, view, search, and delete operations?
  2. The task manager saves tasks as plain text lines. If you wanted to also save a priority level (high, medium, or low) for each task, how would you change the data structure and the file format?
  3. A student gradebook needs to store each student's name and a list of test scores. Draw or describe the nested data structure you would use. What Python type would you choose for the outer container? What about the inner container for scores?
  4. If you are building an application that needs to quickly check whether a username is already taken, which data structure is most efficient: a list, a dictionary, or a set? Why?
TestingTesting Scenarios

For each scenario, write the input you would use, the action you would take, and the expected result.

  1. The contact manager is running with no contacts loaded. The user selects "View all contacts."
  2. The user tries to add a contact by pressing Enter with no name.
  3. The user searches for a name using all lowercase when the stored contact's name uses mixed case.
  4. The user tries to delete a contact that does not exist.
  5. The user quits the program, then relaunches it. All previously added contacts should still be there.
  6. The user enters the letter "x" at the menu prompt.
ProgrammingShort Programming Tasks
  1. The contact manager's 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.
  2. Add an "Edit contact" feature. The user enters the name of a contact to edit, then enters new values for phone and email. If the contact is not found, print a message and return.
  3. The task manager saves tasks as plain text. Modify 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.
  4. Write a count_contacts(contacts) function that prints how many contacts are stored and how many have non-empty email addresses.
  5. Add a confirmation step to delete_contact(). After the contact is found, print "Delete [name]? (y/n): " and only delete if the user confirms with y.
Mini-ProjectSmall Console Application
Mini-Project 28
Build a complete, working console application of your choice
  • FunctionalAt least four menu options (not counting Quit). Stores at least one collection of structured data. Supports adding, viewing, and deleting or editing items. All data saved to a file on quit and loaded on startup.
  • DesignWrite requirements before coding (include as a comment block). Sketch the program flow before coding (include as a comment block). Use at least one class. Keep main() short. Validate all user input — the program must never crash on bad input.
  • DocumentationModule-level docstring describing the program. Docstring on every function and class. Short README file explaining how to run the program and what it does.
  • TestingManual testing checklist with at least eight items. Work through the checklist and fix all failures. Note at least one known limitation.

Suggested Application Ideas

ApplicationWhat It ManagesKey Features
Personal book logBooks read: title, author, ratingAdd, view by rating, search by title
Exercise logSessions: date, activity, durationAdd, view all, total time per activity
Recipe collectionRecipes: name, ingredients, instructionsAdd, search by name, view details
Vocabulary study toolWords and definitionsAdd, quiz mode, view all
Shopping list managerItems with quantity and categoryAdd, mark as bought, view remaining

Chapter Summary

Key Terms

application loop
A while loop in a console application that repeatedly shows a menu, collects input, and performs actions until the user quits.
code documentation
Docstrings and comments inside a program that explain how the code works, what functions expect, and why non-obvious decisions were made.
edge case
An input or situation at the boundary of normal behavior, such as an empty list, a missing file, or a value of zero.
incremental testing
The practice of testing each function as it is written rather than waiting until the entire program is complete.
manual testing checklist
A list of specific inputs and expected results used to verify that an application satisfies its requirements.
menu
A display of numbered or labeled options that a console application presents to the user at each step of the application loop.
persistence
The ability of a program to save data between sessions, usually by writing to a file.
program flow
The sequence of steps a running program follows, from startup through each possible user action to shutdown.
README
A short text file in the same folder as a program that explains how to run it, what it does, and any known limitations.
requirement
A statement of what a program must do, written from the user's perspective before implementation begins.
user documentation
Written instructions for running and using a program, intended for the person who will use it.
user story
A short sentence describing a feature from the user's point of view: "As a user, I want to [do something] so that [I get some benefit]."
ReviewReview Questions

Recall and Vocabulary

  1. What is the difference between a requirement and an implementation decision? Give one example of each for a simple expense tracker.
  2. What is the application loop pattern? Write the general structure of a main() function that uses it, using pseudocode placeholders for features.
  3. Why is it useful to sketch program flow before writing code? What problems can you find during the sketch that you might not find until much later?
  4. Explain the principle of single responsibility as it applies to main() and feature functions. What should main() do, and what should it delegate?
  5. What is the difference between saving contacts as CSV versus saving them as JSON? When might you prefer one over the other?
  6. Why should load_contacts() handle the case where the file does not exist? What would happen without that check?
  7. What is the difference between testing a single function and testing a complete application? Name two things application testing must check that function testing does not.
  8. What belongs in a README file? What belongs in a docstring? Are there things that belong in one but not the other?

Understanding and Explanation

  1. A student writes a contact manager where main() is 80 lines long and contains all the logic for every feature. What problems does this cause? How should the code be reorganized?
  2. You are building a vocabulary quiz application. Write three requirements and three user stories for it. Then sketch the program flow.
  3. Compare these two approaches to checking whether contacts is empty inside view_contacts(): if len(contacts) == 0 and if not contacts. Which is more Pythonic? Are they equivalent?
  4. The 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?

Application and Design

  1. Design a 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.
  2. The chapter's 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?
  3. A classmate says "I don't need to write requirements first — I just start coding and figure it out." Identify two specific problems this approach tends to cause in programs larger than a few dozen lines.
Chapter 28 Checklist
Use this to confirm your understanding before moving to Chapter 29
Planning
  • I can explain how applications differ from exercises in at least three ways.
  • I can write a set of requirements for a simple console application before writing code.
  • I can write user stories in the standard format and map them to requirements.
  • I can sketch a program flow in plain text and identify design decisions it reveals.
Design and Implementation
  • I can write a main() function that implements the application loop pattern and stays short.
  • I can write a menu display function and a menu validation loop.
  • I can choose an appropriate data structure for a given application and justify the choice.
  • I can use a class to represent a data item and standalone functions to operate on collections.
  • I can write save_contacts() and load_contacts() using the csv module.
  • I can handle the first-run case (missing file) gracefully.
Testing and Documentation
  • I can write a manual testing checklist based on my requirements.
  • I can identify edge cases for a console application and decide how to handle them.
  • I can write a README that answers: what does this do, how do I run it, where is the data, what are the limitations?
  • I can write module-level docstrings and function docstrings, and add comments on non-obvious decisions.
Preview — Chapter 29
Testing, Refactoring, and Code Quality

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.

MGU Library Connection
  • Programming with Python — Appendix M: Capstone Project Guide
  • Programming with Python — Appendix I: File and CSV Reference
  • Programming with Python — Appendix K: Object-Oriented Programming Reference
  • Programming with Python — Appendix L: Testing and Refactoring Reference
  • Companion resource: Python Project Checklist
  • Companion resource: Code Style Guide
  • Next chapter: Ch. 29 — Testing, Refactoring, and Code Quality
  • Capstone: Ch. 30 — Building and Documenting a Python Project