MGU Computer Science Series — Programming with Python
Part VI: Object-Oriented and Larger Programs
Testing · Refactoring · Code Quality · Maintenance
Chapter 29 of 30
29
MGU Computer Science Series  ·  Programming with Python  ·  Library Textbook Edition

Chapter 29:
Testing, Refactoring,
and Code Quality

Writing code that works is only part of the job — this chapter teaches you to write code that is readable, testable, and genuinely easy to improve

■ Part VI of VII ■

■ 10 Sections  •  Chapter 29 of 30 ■

Prerequisite: functions, classes, exceptions, and file I/O from earlier chapters

Chapter Introduction and Learning Objectives

By this point in the book, you can write Python programs that use variables, conditionals, loops, functions, data structures, files, exceptions, and classes. You have the vocabulary of a programmer. But writing code that works is only part of the job.

The other part is writing code that is easy to understand, easy to test, easy to fix, and easy to improve. A program can produce correct output and still be nearly impossible to read. It can work today and break the moment someone changes one line. It can have functions that do five unrelated things, variable names that mean nothing, and logic that only the original author could decode.

Professional programmers spend a large portion of their time not writing new code, but improving existing code. They write tests to catch mistakes early. They refactor programs to make the design clearer. They review each other's work. They delete duplication. They rename things. They simplify conditions. They break long functions apart. All of this is called working on code quality, and it matters as much as correctness.

This chapter teaches you how to evaluate the quality of a program, how to write simple tests, and how to improve code through careful refactoring. These skills will serve you in every Python project you work on, including the capstone project in Chapter 30.

Opening Question

Imagine two programs that both calculate the average of a list of scores. Both produce identical output. One has variable names like x, temp2, and stuff, with three copies of the same calculation buried in different places and no tests. The other has named functions like calculate_average and count_passing, a brief docstring on each, and a test file that runs in under a second. If a bug appears six months later, which program would you rather fix? That is the question this chapter answers.

10Sections
11Objectives
14Key Terms
30Total Chapters

Learning Objectives

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

  1. 1Describe the five dimensions of code quality and explain why each matters.
  2. 2Apply naming conventions for variables, functions, Booleans, and classes.
  3. 3Distinguish between comments that explain the why and those that merely restate the what.
  4. 4Write docstrings for functions and explain when they are needed.
  5. 5Write unit tests using assert statements and organize them in a test file.
  6. 6Use assert inside functions to verify assumptions about data.
  7. 7Define refactoring and describe the safe process for doing it.
  8. 8Apply the DRY principle to identify and remove duplicated code.
  9. 9Simplify complex conditions and loops using early returns, named variables, and idiomatic Python patterns.
  10. 10Organize a multi-file Python project with clear module structure and a README.
  11. 11Apply a code review checklist to evaluate and improve a program.

Sections at a Glance

Sections 29.1 – 29.3
The Foundation: Quality, Names, and Documentation
Correctness · Readability · Naming · Comments · Docstrings

What Code Quality Means

When programmers talk about code quality, they are not talking about whether the code runs. They are talking about whether the code is good to work with.

Consider two programs that both calculate the average of a list of numbers. Both produce the same output. But one is twenty lines of tangled logic with variable names like x, temp2, and stuff, with no comments and three copies of the same calculation spread through the code. The other is eight lines of clean functions with names like calculate_average and count_items, a brief comment explaining the purpose, and no repeated logic.

If you had to fix a bug in one of those programs, you would want the second one. If you had to add a feature, you would want the second one. If someone else had to maintain the code for the next two years, they would want the second one.

Code quality is about the human side of programming. Code is read far more often than it is written. Every program you write is a message to a future reader — and that future reader is often yourself, six months from now, with no memory of what you were thinking.

Dimension 1
Correctness
The program does what it is supposed to do, including edge cases. A program that produces wrong output has no quality at all, no matter how beautiful the code is.
Dimension 2
Readability
The code is easy to read and understand without extensive explanation. A reader should be able to trace the logic without asking the author for help.
Dimension 3
Simplicity
The code is no more complicated than it needs to be. Every extra layer of indentation, every unnecessary variable, and every clever trick is a cost the reader pays.
Dimension 4
Maintainability
The code is easy to modify without breaking things. When requirements change — and they always change — a maintainable program bends; a fragile one breaks.
Dimension 5
Testability
The code can be verified systematically. Functions that do one thing with clear inputs and outputs are easy to test. Functions that do everything at once are not.
Why These Five Reinforce Each Other

Simple code is usually more readable. Readable code is easier to test. Testable code is easier to maintain. Well-maintained code tends to stay correct longer. Improving any one dimension tends to pull the others upward.

Readability and Naming

The single most powerful thing you can do to improve code quality is to use good names. Python allows almost any name for a variable, function, or class. That freedom is dangerous. A variable named x tells you nothing. A variable named student_average_score tells you exactly what it holds.

Rules for Good Names

Poor Names
x = 85
y = 0
for i in items:
    y += i
Better Names
passing_grade = 85
total_score = 0
for score in scores:
    total_score += score

Variables should describe what they store, not how they are used.

Functions should describe what they do, ideally starting with a verb.

Poor Function Names
def data(lst):
    ...

def process(x):
    ...
Better Function Names
def calculate_average(scores):
    ...

def find_highest_score(scores):
    ...

Boolean variables should read naturally as true or false.

Boolean Naming Examples
# Awkward
status = True

# Natural — reads like a true/false question
is_logged_in = True
has_errors = False
account_is_active = True

Classes should be nouns in CapWords style. For example: StudentRecord, ShoppingCart, ContactBook.

Avoiding Misleading Names

Misleading names are worse than no names at all. A variable called total that actually stores an average will cause real confusion. A function called print_report that also saves a file and sends an email is lying about what it does. Names should match behavior exactly.

Misleading vs. Honest Function Names
# This function is lying — it does far more than it claims
def print_report(data):
    print(data)
    save_to_file(data)       # surprise!
    send_email(data)         # another surprise

# Better: give each responsibility its own honest function
def print_report(data):
    print(data)

def save_report(data):
    save_to_file(data)

def send_report(data):
    send_email(data)

Line Length and Spacing

Python code reads best when lines stay under 80–90 characters. Long lines force readers to scroll sideways and make it harder to see the structure of an expression. Use blank lines to separate logical sections within a function. Use two blank lines between function definitions. Spacing is not decoration — it shows the reader where one idea ends and another begins.

Comments and Docstrings

When to Comment

Comments explain the why, not the what. Good code mostly explains itself. Comments are for the parts that do not.

Poor Comment — Restates the Code
total = total + score
# add score to total
Good Comment — Explains Why
if score >= 60:    # district policy
    grade = "Pass"

If a comment is just restating the code in English, delete it. It adds noise without adding understanding.

Docstrings

A docstring is a string literal placed at the start of a function, class, or module. It explains what the function does, what it accepts, and what it returns. Docstrings are accessible through Python's built-in help() system.

Full Docstring Format
def calculate_average(numbers):
    """
    Return the arithmetic mean of a list of numbers.

    Args:
        numbers: A list of numeric values. Must not be empty.

    Returns:
        The average as a float.

    Raises:
        ValueError: If the list is empty.
    """
    if not numbers:
        raise ValueError("Cannot average an empty list.")
    return sum(numbers) / len(numbers)

The one-line docstring form is fine for simple functions:

One-Line Docstring
def is_passing(score):
    """Return True if score is 60 or higher."""
    return score >= 60

Comments to Avoid

Avoid Commented-Out Code

Do not leave blocks of dead code in a file permanently. If code is no longer needed, delete it. If you might need it later, version control tools like Git can retrieve it. Commented-out code is confusing: readers cannot tell whether it was intentionally removed, forgotten, or still important.

Sections 29.4 – 29.5
Verification: Unit Tests and Assertions
Automated Tests · assert · Test Files · Assumption Checking

Unit Tests at an Introductory Level

A unit test is a small, automated check that verifies a specific piece of code produces the expected output for a given input. You have been testing programs throughout this book, but mostly by running them and reading the output yourself. That is called manual testing, and it is valuable — but it has two weaknesses. First, you have to remember to repeat it every time you change something. Second, humans miss things.

Automated unit tests solve both problems. You write them once, and you can run them any time to confirm everything still works.

Using assert Statements

The assert statement checks whether a condition is true. If it is, nothing happens. If it is false, Python raises an AssertionError.

Simple assert Tests
assert calculate_average([10, 20, 30]) == 20.0
assert calculate_average([100]) == 100.0
assert calculate_average([0, 0, 0]) == 0.0
print("All tests passed.")

# A failing assertion stops the program immediately:
assert calculate_average([10, 20]) == 15.5    # actual: 15.0 → AssertionError

Testing for Exceptions

Testing That an Exception Is Raised
def test_empty_list_raises():
    try:
        calculate_average([])
        print("FAIL: No exception was raised.")
    except ValueError:
        print("PASS: ValueError raised correctly.")

test_empty_list_raises()

Organizing Tests in a Test File

As programs grow, collect all tests in one file separate from the main program.

FileContents
grades.pyMain functions: calculate_average, is_passing, letter_grade
test_grades.pyTest functions for each function in grades.py
test_grades.py — A Complete Test File
from grades import calculate_average, is_passing, letter_grade


def test_calculate_average():
    assert calculate_average([10, 20, 30]) == 20.0
    assert calculate_average([100]) == 100.0
    assert calculate_average([0, 0, 0]) == 0.0
    print("test_calculate_average passed.")


def test_is_passing():
    assert is_passing(60) == True
    assert is_passing(59) == False
    assert is_passing(100) == True
    assert is_passing(0) == False
    print("test_is_passing passed.")


def test_letter_grade():
    assert letter_grade(95) == "A"
    assert letter_grade(85) == "B"
    assert letter_grade(75) == "C"
    assert letter_grade(65) == "D"
    assert letter_grade(55) == "F"
    print("test_letter_grade passed.")


# Run all tests
test_calculate_average()
test_is_passing()
test_letter_grade()

What Makes a Good Unit Test

Quality 1
Specific
Each test checks one thing. A test that checks five conditions at once is hard to diagnose when it fails.
Quality 2
Readable
The expected behavior is obvious from reading the test itself, without needing to consult the function being tested.
Quality 3
Repeatable
The test produces the same result every time, regardless of when or how often it is run.
Quality 4
Complete
Tests cover normal cases, boundary cases (exactly at a threshold), and error cases (bad input).

Assertions

The assert statement is useful beyond test files. You can use assertions inside functions to verify that your assumptions about the data are correct when the program runs.

Assertions Inside a Function
def calculate_average(numbers):
    """Return the arithmetic mean of a list of numbers."""
    assert isinstance(numbers, list), "Input must be a list."
    assert len(numbers) > 0, "List must not be empty."
    return sum(numbers) / len(numbers)

If someone passes in a string or an empty list, Python immediately raises a clear AssertionError with a message rather than failing silently somewhere deeper in the program.

Assertions Are Not Input Validation

Assertions are for catching programming errors, not for handling user input. If your program accepts data from a user and that data might be invalid, use if statements and raise to give a proper error. Assertions can be disabled with a Python runtime flag. User-facing validation should never be disabled.

Wrong — assert for User Input
def process_age(age):
    assert age >= 0, "Age cannot be negative."
Right — raise for User Input
def process_age(age):
    if age < 0:
        raise ValueError(
            f"Age cannot be negative. Got: {age}"
        )
Sections 29.6 – 29.8
Improvement: Refactoring, DRY, and Simplification
Refactoring · DRY · Early Returns · Idiomatic Loops

Refactoring

Refactoring means improving the internal structure of code without changing its external behavior. The program does exactly the same thing before and after — it just does it in a cleaner, more organized way. You refactor when code is hard to read, functions are too long, the same logic appears in more than one place, names are unclear, or a function does too many unrelated things.

The Safe Refactoring Process

Step 1. Make sure the code has tests, so you can confirm nothing breaks.
Step 2. Make one small improvement.
Step 3. Run the tests to confirm behavior is unchanged.
Step 4. Repeat.

Refactoring in large steps without tests is risky. Small steps with tests are safe.

Example: Refactoring a Long Function

Before Refactoring — One Large Function
def run_report(filename):
    scores = []
    with open(filename) as f:
        for line in f:
            parts = line.strip().split(",")
            scores.append(int(parts[1]))

    total = 0
    for score in scores:
        total += score
    average = total / len(scores)

    passing = 0
    for score in scores:
        if score >= 60:
            passing += 1

    print(f"Average score: {average:.1f}")
    print(f"Students passing: {passing}")
    print(f"Students in file: {len(scores)}")
After Refactoring — Each Function Does One Thing
def load_scores(filename):
    """Read a CSV file and return a list of integer scores."""
    scores = []
    with open(filename) as f:
        for line in f:
            parts = line.strip().split(",")
            scores.append(int(parts[1]))
    return scores


def calculate_average(scores):
    """Return the mean of a list of scores."""
    if not scores:
        raise ValueError("Score list is empty.")
    return sum(scores) / len(scores)


def count_passing(scores, threshold=60):
    """Return the number of scores at or above the threshold."""
    return sum(1 for score in scores if score >= threshold)


def print_report(scores):
    """Print a summary report for a list of scores."""
    average = calculate_average(scores)
    passing = count_passing(scores)
    print(f"Average score: {average:.1f}")
    print(f"Students passing: {passing}")
    print(f"Students in file: {len(scores)}")


def run_report(filename):
    """Load data and print the full report."""
    scores = load_scores(filename)
    print_report(scores)
What Changed

The behavior of the program is identical. But now each function does exactly one thing, each can be tested independently, and changes to the file format touch only load_scores while changes to the passing threshold touch only count_passing. The structure now matches the problem.

Removing Duplication

Duplicated code is one of the most common quality problems in beginner programs. When the same logic appears in two or more places, changing it requires finding and updating every copy. Missing even one copy creates a bug. The rule programmers use is called DRY: Don't Repeat Yourself.

Before — Grading Logic Duplicated in Two Functions
def process_final_exam(score):
    if score >= 90:
        grade = "A"
    elif score >= 80:
        grade = "B"
    elif score >= 70:
        grade = "C"
    elif score >= 60:
        grade = "D"
    else:
        grade = "F"
    return grade


def process_midterm(score):    # identical grading logic
    if score >= 90:
        grade = "A"
    elif score >= 80:
        grade = "B"
    ...
    return grade
After — Logic Extracted Once
def letter_grade(score):
    """Return the letter grade for a numeric score."""
    if score >= 90:
        return "A"
    elif score >= 80:
        return "B"
    elif score >= 70:
        return "C"
    elif score >= 60:
        return "D"
    else:
        return "F"


def process_final_exam(score):
    return letter_grade(score)


def process_midterm(score):
    return letter_grade(score)

Now the grading logic exists in exactly one place. If the thresholds ever change, you change them once.

Duplication in Conditions

Repeated Condition — Extract It
# Repeated condition in two places
if temperature > 30 and humidity > 70:
    print("Heat advisory issued.")
    log_entry("Heat advisory")

if temperature > 30 and humidity > 70:
    update_status("Alert")

# Better: name the condition once
heat_advisory = temperature > 30 and humidity > 70

if heat_advisory:
    print("Heat advisory issued.")
    log_entry("Heat advisory")
    update_status("Alert")

Simplifying Conditions and Loops

Simplifying Complex Conditions

Named Condition Variables
# Hard to read as one long condition
if age >= 18 and income >= 30000 and credit_score >= 650 and not has_existing_loan:
    approve_loan()

# Easier to read — each piece has a meaningful name
old_enough = age >= 18
sufficient_income = income >= 30000
good_credit = credit_score >= 650
no_existing_debt = not has_existing_loan

if old_enough and sufficient_income and good_credit and no_existing_debt:
    approve_loan()

Early Returns to Reduce Nesting

Deeply Nested
def process_order(order):
    if order is not None:
        if order["status"] == "pending":
            if order["total"] > 0:
                fulfill_order(order)
Flat with Early Returns
def process_order(order):
    if order is None:
        return
    if order["status"] != "pending":
        return
    if order["total"] <= 0:
        return
    fulfill_order(order)

Early returns remove nesting and make the main path of the function clearer. Readers reach fulfill_order knowing all conditions have been verified.

Simplifying Loops

Common Loop Simplifications
# Use _ when the loop variable is unused
for _ in range(3):
    print("Retry")

# Iterate directly — avoid manual index when you don't need it
for name in names:        # prefer this
    print(name)

# Use enumerate when you need both index and value
for i, name in enumerate(names):
    print(f"{i + 1}. {name}")

# Never modify a list while iterating — build a new one instead
inventory = [item for item in inventory if item["quantity"] > 0]
Sections 29.9 – 29.10
Organization and Review
Project Files · README · Code Review · Maintenance

Organizing Project Files

As programs grow beyond a single file, organization matters. A well-organized project is easier to navigate, easier to test, and easier to hand to someone else.

Basic Single-File Programs

For simple programs, one file is fine: calculator.py, quiz.py, notes.py. When the program has several distinct components, separate them into modules.

Example: Multi-Module Project Structure
gradebook/
    gradebook.py          ← main program, menus, user interaction
    grades.py             ← grade calculation functions
    reports.py            ← report generation functions
    file_io.py            ← file reading and writing
    test_grades.py        ← tests for grades.py
    test_reports.py       ← tests for reports.py
    data/
        students.csv      ← sample data
    README.md             ← description and instructions
FilePurpose
main.py or program nameEntry point, menus, user interaction
Descriptive module nameClasses, data definitions, or business logic
file_io.pyFile reading, writing, CSV handling
test_*.pyTests for the corresponding module
README.mdDescription, setup instructions, usage

README Files

A README.md file belongs at the top of every project. It should describe what the program does, how to run it, what files are included, any setup steps required, and known limitations. A short README is always better than no README.

Example README.md
# Student Gradebook

A command-line program for managing student scores and generating reports.

## How to Run

    python gradebook.py

## Files

- gradebook.py — main program
- grades.py — grade calculation functions
- data/students.csv — sample student data

## Requirements

Python 3.8 or later. No external packages required.

Code Review and Maintenance

What Code Review Is

Code review is the practice of having another person — or yourself, with fresh eyes — read your code before it is finalized. In professional software development, almost no code is released without review. The purpose is not to criticize the programmer; it is to catch problems the original author could not see because they were too close to the work.

Self-review works better than you might expect, especially if you wait at least a few hours before reviewing. When you return to code with fresh eyes, you will notice things you missed when writing it.

Questions to Ask When Reviewing Your Own Code

Self-Review Guide
Work through each question before considering a program finished
  • ReadabilityCan I read each function and immediately understand what it does? Are the names clear and honest?
  • DuplicationIs any logic duplicated? If I needed to change a rule, how many places would I need to update?
  • ResponsibilityAre there functions that do more than one thing? Can I split them?
  • DocumentationAre there comments that explain the why? Do shared functions have docstrings? Is there any dead commented-out code?
  • TestsAre there tests for the important functions? Do the tests cover edge cases and error cases?
  • Edge CasesDoes the program handle empty input, zero, very large values, and invalid types?
  • HandoffWould someone else be able to run and extend this program using only the README and the code?

On Maintaining Code Over Time

Programs that work today can break tomorrow when the input format changes, a library is updated, a new Python version handles something differently, the requirements change, or a bug is found in a corner case that was never tested. Maintainable code handles these changes gracefully. A program that has never been touched again after it was written is not typical. Real programs are updated, extended, handed to new people, and adapted to new situations. Writing code as if someone will have to read and change it later — because someone will — is one of the most valuable habits a programmer can develop.

Improving a Program End-to-End

Here is a working program that calculates student grades. It produces correct output but has several quality problems. Study the original, then trace every improvement made in the refactored version.

Original — Works, but Has Many Quality Problems
def f(d):
    r = []
    for x in d:
        n = x[0]
        s = (x[1] + x[2] + x[3]) / 3
        if s >= 90:
            g = 'A'
        elif s >= 80:
            g = 'B'
        elif s >= 70:
            g = 'C'
        elif s >= 60:
            g = 'D'
        else:
            g = 'F'
        r.append((n, s, g))
    return r

data = [
    ("Alice", 88, 92, 85),
    ("Bob",   55, 60, 48),
    ("Carol", 95, 97, 91),
    ("David", 70, 74, 68),
]

results = f(data)
for item in results:
    print(item[0], round(item[1], 1), item[2])
Problems Identified
Every quality problem in the original program
  • Namesf, d, r, x, n, s, g are all single-letter or meaningless names. None describe what they hold.
  • Mixed concernsThe single function loads data, calculates an average, determines a letter grade, and builds the results list — all at once.
  • No docstringsNo function has any documentation explaining its purpose, inputs, or outputs.
  • No testsThere are no assertions or test functions to verify behavior.
  • Index accessThe output code uses item[0], item[1] instead of named access, making it impossible to understand what each position means.
Refactored — Clean, Tested, and Documented
def letter_grade(average):
    """Return the letter grade for a numeric average."""
    if average >= 90:
        return "A"
    elif average >= 80:
        return "B"
    elif average >= 70:
        return "C"
    elif average >= 60:
        return "D"
    else:
        return "F"


def calculate_student_result(name, score1, score2, score3):
    """
    Calculate the average and letter grade for a student.

    Returns a dictionary with keys: name, average, grade.
    """
    average = (score1 + score2 + score3) / 3
    grade = letter_grade(average)
    return {"name": name, "average": average, "grade": grade}


def process_class(student_data):
    """Process a list of student records and return results."""
    results = []
    for name, score1, score2, score3 in student_data:
        result = calculate_student_result(name, score1, score2, score3)
        results.append(result)
    return results


def print_results(results):
    """Print a formatted summary of student results."""
    for result in results:
        print(f"{result['name']:10}  {result['average']:.1f}  {result['grade']}")


# --- Tests ---

def test_letter_grade():
    assert letter_grade(95) == "A"
    assert letter_grade(85) == "B"
    assert letter_grade(90) == "A"    # boundary: exactly 90
    assert letter_grade(60) == "D"    # boundary: exactly 60
    assert letter_grade(55) == "F"
    print("test_letter_grade passed.")


def test_calculate_student_result():
    result = calculate_student_result("Alice", 88, 92, 85)
    assert result["name"] == "Alice"
    assert abs(result["average"] - 88.333) < 0.01
    assert result["grade"] == "B"
    print("test_calculate_student_result passed.")


# --- Main program ---

student_data = [
    ("Alice", 88, 92, 85),
    ("Bob",   55, 60, 48),
    ("Carol", 95, 97, 91),
    ("David", 70, 74, 68),
]

test_letter_grade()
test_calculate_student_result()

results = process_class(student_data)
print_results(results)
Both Programs Produce the Same Output

The refactored program is not faster or more featureful. It is the same program expressed in a way that is vastly easier to read, test, and change. That is what code quality means.

Chapter Summary

Key Terms

assertion
A statement using assert that verifies a condition is true; raises AssertionError if it is false. Used to verify assumptions about data during development.
automated test
A piece of code that verifies the behavior of another piece of code without requiring manual inspection of output. Can be re-run any time to confirm nothing has broken.
code quality
A measure of how readable, maintainable, testable, and correct a program is — independent of whether it currently runs.
code review
The practice of reading and evaluating code, either by another person or by yourself after a break, to catch problems and improve quality.
docstring
A string literal placed at the start of a function, class, or module to describe its purpose, arguments, and return value. Accessible through Python's help().
DRY
Don't Repeat Yourself — a principle stating that each piece of logic should exist in exactly one place in a codebase.
early return
A return statement placed at the start of a function to handle special cases and reduce nesting, making the main logic path easier to follow.
edge case
An input or condition at the boundary of what a function is designed to handle: an empty list, zero, a maximum value, a very long string, or invalid types.
maintainability
The quality of code that makes it easy to understand, modify, and extend over time without breaking existing behavior.
manual testing
Testing a program by running it and observing the output yourself. Valuable but unrepeatable without effort and prone to human oversight.
refactoring
Improving the internal structure of code without changing its external behavior. Safe refactoring always requires tests to confirm nothing breaks.
single responsibility
The principle that each function or class should do exactly one thing and do it well. Functions that do many things are harder to test, name, and reuse.
test case
A specific input paired with an expected output used to verify that a function behaves correctly for that input.
unit test
A test that verifies the behavior of a single, isolated function or component. Unit tests are the foundation of automated testing.
PracticeCode Reading Practice

Read each code example carefully. Identify problems and reason through the expected behavior before writing anything.

  1. Exercise 1. Read this function and describe one quality problem. Then rewrite it to fix the problem.

    def p(l): t = 0 / for x in l: t = t + x / a = t / len(l) / print(a)
  2. Exercise 2. The following two functions contain identical logic. Identify the duplication. Write a third function that extracts the shared logic, and rewrite both original functions to call it.
  3. Exercise 3. What is wrong with each of these test assertions? Identify the specific error in each.

    Assertion A: assert calculate_average([10, 20, 30])
    Assertion B: assert calculate_average == 20.0
    Assertion C: assert calculate_average([10, 20, 30]) = 20.0
PracticeCode Tracing Practice
  1. Exercise 4. Trace the following test code. What prints if letter_grade is implemented correctly? What happens if the function erroneously returns "B" for a score of exactly 90? Which assert line fails, and what does the error message show?
  2. Exercise 5. Compare these two versions of approve_loan — one deeply nested, one using early returns. Trace both with inputs (20, 35000, 700) and (16, 35000, 700). Do they produce the same result? At which line does each version make its decision?
PracticeDebugging Practice
  1. Exercise 6. A test file has this line: assert is_passing(59) == True. The function is_passing returns score >= 60. The test always fails. Is the bug in is_passing or in the test? Explain your reasoning.
  2. Exercise 7. This function has a quality problem that produces incorrect output without raising an error: def calculate_discount(price, percent): discount = price * percent; return price - discount. Write a test that catches this bug. Then fix the function.
  3. Exercise 8. A program has three code quality problems even though it produces correct output. Identify each problem and describe how to fix it: (a) a function named go with parameters a, b, c, d; (b) a condition written as if x == True; (c) four levels of nesting where early returns would flatten the logic.
PracticeShort Programming Tasks
  1. Task 1. Write a function called clamp(value, minimum, maximum) that returns value if it is between minimum and maximum, returns minimum if too low, and returns maximum if too high. Write at least five assert tests covering normal cases, boundary values, and at least one case where clamping occurs in each direction.
  2. Task 2. You are given two functions — print_invoice(items) and print_receipt(items) — that contain nearly identical logic: iterate over items, print each line total, sum the total, and print a header and footer with different titles. Extract the shared logic into a helper function and rewrite both functions to use it without changing the output.
  3. Task 3. Write a test file for a budget.py module containing three functions: is_over_budget(spent, budget), remaining_budget(spent, budget), and budget_percentage_used(spent, budget). Your tests should cover normal cases, boundary cases, and at least one case that should raise a ValueError.
  4. Task 4. Refactor this function to use early returns and meaningful parameter names. The behavior must not change: a function check_eligibility(a, b, c, d) where a = is a citizen, b = age, c = has valid ID, d = outstanding violations. The function returns True only when all four conditions are satisfied.
  5. Task 5. Take any mini-project you completed in an earlier chapter and apply the full code review checklist from Section 29.10. List every problem you find. Fix at least three and add at least two tests.
Code Quality Checklist
Apply this checklist to every Python program before considering it finished
Names
  • All variable names describe what they store, not how they are used.
  • All function names start with a verb and honestly describe behavior.
  • No single-letter names except conventional loop variables like i or _.
  • No misleading or ambiguous names anywhere in the file.
  • Boolean variables read naturally as true or false questions.
Functions
  • Each function does exactly one thing.
  • Each function is short enough to understand at a glance (typically under 20–30 lines).
  • No logic is duplicated across two or more functions.
  • Functions return results rather than printing them where appropriate.
  • Return values are used correctly by callers.
Documentation
  • Non-obvious logic has a comment explaining the why, not restating the what.
  • Reused or shared functions have docstrings.
  • No dead commented-out code remains in the file.
Logic
  • Conditions are readable; complex conditions are extracted into named variables.
  • Nesting is reduced with early returns where appropriate.
  • Loops iterate directly over sequences rather than using manual indexing.
  • Edge cases are handled: empty collections, zero, negative numbers, invalid types.
Tests
  • Key functions have at least one test.
  • Tests cover normal input, boundary values, and error conditions.
  • Tests are collected in a dedicated test file.
  • All tests pass before the program is considered complete.
Organization
  • Related code is grouped logically within each file.
  • Larger projects separate concerns into distinct modules.
  • A README or description file is present at the project root.
Mixed ReviewMixed Review
  1. Explain the difference between a syntax error, a runtime error, a logic error, and a code quality problem. Can a program have good code quality and still have a logic error? Explain.
  2. A classmate says: "My program works, so I don't need to refactor it." Write a response that explains what refactoring is for, and give two examples of why a working program might still benefit from refactoring.
  3. What does DRY stand for, and why does violating it create problems? Give an example of duplicated code and show how to remove the duplication.
  4. Describe three situations in which an assertion inside a function is appropriate and one situation in which you should use raise ValueError instead. Explain the difference.
  5. You are given a function with the signature process_data(d). Without reading the body, what can you tell from the name alone? What makes this signature a quality problem, and what would be a better version?
ReviewReview Questions

Recall and Vocabulary

  1. What are the five dimensions of code quality described in Section 29.1? Define each one in a sentence.
  2. Why should function names start with a verb? Give one example of a poor function name and one example of a better replacement.
  3. What is the difference between a comment and a docstring? When should each be used?
  4. What does the assert statement do? What happens when an assertion fails?
  5. What is a unit test, and what are the four characteristics of a good unit test?
  6. What does refactoring mean? What must you have in place before refactoring safely?
  7. What is the DRY principle? Give one concrete example of code that violates it.
  8. What is an early return, and why does it reduce nesting?
  9. Name three things that should appear in a README file.
  10. What is code review? What is its purpose?

Understanding and Explanation

  1. Why is code readability important if the program is only ever run by a machine? Who actually reads the code?
  2. Explain why a function that does five things is harder to test than five functions that each do one thing.
  3. Why are misleading names worse than vague names? Give an example that illustrates the difference.
  4. A classmate argues: "Writing tests takes time I could spend writing features." Write a response that explains what tests are actually for and why they save time in the long run.
  5. You have two identical if/elif/else blocks in two different functions. How does the DRY principle tell you to fix this, and what happens if you do not?

Application and Analysis

  1. Apply the code quality checklist to a program you have written in a previous chapter. List every item that fails. Write a plan for fixing at least three items.
  2. A function currently has 60 lines of code and does the following: reads a file, filters rows, calculates a summary, and prints a report. Describe how you would refactor it. Name the resulting functions and explain what each one does.
  3. Write the test functions you would want to have before refactoring the 60-line function in Question 17. What inputs would you test? What outputs would you assert?
Capstone Preparation
Apply the skills of this chapter before beginning Chapter 30
  • Review earlier workChoose one mini-project you completed in a previous chapter and apply the full code quality checklist to it. List every item that fails.
  • Write testsWrite a test function for each main function in your chosen project. Run the tests to confirm they all pass before making changes.
  • RefactorFix at least three quality problems identified by the checklist. Run your tests after each fix to confirm nothing broke.
  • DocumentAdd docstrings to all functions that lack them. Add a README file describing what the program does and how to run it.
  • ReflectCompare the program before and after. What changed? What was hardest to improve? What would you do differently when starting the capstone project?
Preview — Chapter 30
Building and Documenting a Python Project

Chapter 30 is the capstone of this textbook. You will plan, build, test, document, and present a complete Python project that combines the major skills of all seven parts of the book — values and variables, control flow, functions, data structures, files, exception handling, and where appropriate, classes. The project will be tested with the techniques from this chapter, refactored for quality, and documented with a README and docstrings. Chapter 29 has given you the habits of a careful programmer. Chapter 30 gives you the opportunity to practice all of them on a project that is entirely your own.

MGU Library Connection
  • Programming with Python — Appendix L: Testing and Refactoring Reference
  • Programming with Python — Appendix F: Code Style Guide
  • Companion resource: Debugging Checklist
  • Companion resource: Testing Checklist
  • Companion resource: Refactoring Checklist
  • Companion resource: Capstone Project Rubric
  • Next chapter: Chapter 30 — Building and Documenting a Python Project