MGU Computer Science Series — Computer Science Foundations
Part II: Programming Foundations
Syntax Errors · Runtime Errors · Logic Errors · Debugging · Testing · Program Design
Chapter 10 of 28
MGU Computer Science Series  ·  Computer Science Foundations  ·  Library Textbook Edition

Chapter 10:
Debugging, Testing,
and Program Design

Finding mistakes, designing tests, building programs incrementally, and writing code that communicates — the habits that turn programs that seem to work into programs that actually do

■ Part II: Programming Foundations ■

■ 10 Sections  •  Chapter 10 of 28 ■

Closes Part II · Follows Chapter 9: Functions, Decomposition, and Reuse · Prepares for Part III: Data and Algorithms

Purpose and Overview

"Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?"

Brian Kernighan, computer scientist

Programs rarely work correctly the first time. This is not a sign that you are doing something wrong — it is the normal, expected, universal experience of every programmer who has ever lived. From beginners writing their first ten-line program to expert engineers building systems with millions of lines of code, mistakes happen. The question is not how to avoid them entirely, but how to find them, understand them, and fix them.

This chapter teaches you to do exactly that. You will learn the three major categories of programming errors, how to read the messages a programming environment produces when something goes wrong, and a set of reliable strategies for finding mistakes when error messages are not enough. You will also learn how to test programs deliberately and thoroughly — not just running them once and hoping, but designing specific tests that check whether your code handles all the situations it will encounter. Finally, you will learn about program design: how to think about structure and organization before and during the writing of code, so that programs are easier to read, easier to test, and easier to fix.

These skills — debugging, testing, and design — are what separate programs that merely seem to work from programs that actually do.

Opening Question

You write a program that calculates student averages. You run it, it produces numbers, and the numbers look reasonable. Is the program correct? How would you know? You would need to check specific inputs against known expected outputs, test what happens with edge cases like an empty list or a list with a single score, and verify that the logic handles every boundary correctly. By the end of this chapter, you will have a systematic method for answering that question — for any program you write.

10Sections
3Error Categories
7Debugging Strategies
15Key Terms

Learning Objectives

  1. 1Explain why programming mistakes are inevitable and describe the right attitude toward them.
  2. 2Define and distinguish syntax errors, runtime errors, and logic errors.
  3. 3Identify common examples of each error category in Python-style programs.
  4. 4Read and interpret Python error messages and tracebacks.
  5. 5Apply a toolkit of debugging strategies including hand tracing, print debugging, and assumption checking.
  6. 6Design deliberate test cases with specific inputs and expected outputs.
  7. 7Identify edge cases for a given function and explain why they matter.
  8. 8Describe the incremental development approach and explain why it reduces debugging difficulty.
  9. 9Write useful comments and docstrings for functions and programs.
  10. 10Apply program design thinking before and during writing code.

Sections at a Glance

Sections 10.1 – 10.5
Mistakes, Error Categories, and Error Messages
Why Mistakes Happen · Syntax Errors · Runtime Errors · Logic Errors · Reading Tracebacks

Mistakes Are Part of Programming

Before learning how to fix mistakes, it helps to understand what mistakes actually are and why they happen so consistently. A program is a precise set of instructions. Computers follow those instructions exactly as written — not as intended, not as assumed, but exactly as written. This creates an immediate source of tension: human beings are imprecise thinkers who work with intentions, and computers are precise executors that work with instructions. The gap between intention and instruction is where almost every programming mistake lives.

Where Mistakes Come From

SourceDescriptionExample
ComplexityMany interacting parts — variables, loops, functions, conditions — each a potential failure pointA variable updated in one function has unexpected effects in another
Invisible assumptionsUnstated beliefs about the program's environment that live in the programmer's head, not the codeAssuming a list will never be empty; assuming user input will be a number
Translation difficultyMoving from a human description to precise code requires many small decisions, any of which can be wrong"Calculate the average" requires deciding how to handle zero count, rounding, and input format
AccumulationA small mistake early in a program can produce effects only visible much laterWrong initialization on line 5 causes a crash on line 40
The Right Attitude

Experienced programmers make mistakes constantly. What distinguishes them is not that they make fewer mistakes — it is that they have good strategies for finding and fixing them efficiently. Debugging is a skill. When something goes wrong, a program is telling you something. Your job is to figure out what it is telling you.

Syntax Errors

Every programming language has a set of rules called its syntax — the grammar of the language. A syntax error occurs when code violates these rules. Syntax errors are caught before the program runs: Python reads the code, detects the violation, reports it, and refuses to execute anything. You cannot have a running program with a syntax error in it.

This makes syntax errors the easiest category to deal with. They are caught immediately, they tell you roughly where the problem is, and fixing them is usually a matter of correcting a typo or misplaced character.

Common Syntax Errors

Syntax Error 1
Missing colon after if, for, while, or def
Error
if score > 90
    print("Excellent")
Correct
if score > 90:
    print("Excellent")
Syntax Error 2
Mismatched parentheses or brackets
Error — missing closing parenthesis
result = (3 + 4 * 2
Correct
result = (3 + 4 * 2)
Syntax Error 3
Incorrect indentation
Error — body of if is not indented
if temperature > 100:
print("Very hot")
Correct
if temperature > 100:
    print("Very hot")
Syntax Error TypeWhat to Look For
Missing colonEnd of if, elif, else, for, while, def
Mismatched delimitersCount opening and closing (), [], {}
Wrong indentationConsistent four-space indent inside every block
Misspelled keywordTrue not true; elif not elseif; def not Def
Unclosed stringEvery " or ' must have a matching closing quote
Debugging Principle

When fixing a syntax error, read slowly. Many syntax errors are a single missing or wrong character. Train your eye to look for what is absent, not just what is present. Also note: the line Python reports is where it noticed the error — check a few lines before that line if the reported line looks correct.

Runtime Errors

A runtime error occurs while a program is running, when a syntactically correct operation cannot be completed. The program starts, runs until it reaches an impossible situation, then crashes and produces an error message called an exception. Runtime errors depend on conditions: the same line of code might run successfully one hundred times and fail on the hundred-and-first, when conditions happen to be different.

Common Runtime Errors

Runtime Error — ZeroDivisionError
Division by zero
  • Codeaverage = total / count
  • When it failsWhen count is 0. If count is 5, the line runs fine. If count is 0, Python raises ZeroDivisionError and stops.
  • Why it happensThe code is syntactically legal. The bug is a condition that the programmer did not guard against.
Error TypeMeaningCommon Cause
ZeroDivisionErrorA number was divided by zeroUnguarded division where denominator can be 0
IndexErrorA list or string position does not existscores[3] when list has only 3 items (indices 0–2)
NameErrorA variable or function has not been definedUsing a variable before assigning it a value
TypeErrorAn operation was used on the wrong type"Score: " + 85 — cannot add string and integer
FileNotFoundErrorA file at the given path does not existTypo in filename, file in wrong folder
KeyErrorA dictionary key does not existAccessing a key that was never added to the dictionary
ValueErrorRight type, wrong contentint("hello") — cannot convert a word to an integer
Debugging Principle

When you encounter a runtime error, do not just look at the crashing line. Trace backward. Ask how the values in play got to where they are. A variable might be assigned the wrong value twenty lines earlier, causing a crash only when that value is finally used.

Logic Errors

A logic error is a mistake in the reasoning of a program. The program has correct syntax. It runs without crashing. It produces output. But the output is wrong. Logic errors do not produce error messages. No alarm sounds. No exception is raised. The program runs to completion and silently delivers wrong results — this is what makes them the hardest category to find.

Common Logic Errors

Logic Error 1
Off-by-one error — loop boundary wrong by exactly one
Intended: print 1 through 10. Actual: prints 0 through 9
for i in range(10):
    print(i)

The code is legal. Python produces no error. But the intent was range(1, 11). Off-by-one errors are among the most common logic errors in all of programming.

Logic Error 2
Wrong operator — boundary condition uses > instead of >=
Intended: pass if score is 60 or higher. A score of exactly 60 fails.
if score > 60:
    print("Pass")

60 > 60 is False. The boundary should be >=. The program runs and produces plausible output. The bug is invisible until you test the exact boundary value.

Logic Error 3
Wrong variable — divides total by itself instead of count
average is always 1.0 because total / total = 1
total = 0
count = 0
for value in scores:
    total = total + value
    count = count + 1
average = total / total   # Should be: total / count
Logic Error 4
Missing update inside a loop — largest never changes
The comparison runs but the update never happens
largest = scores[0]
for score in scores:
    if score > largest:
        pass   # Forgot: largest = score
print("Largest:", largest)
Why Logic Errors Are Hard to Find

Logic errors are hard because the programmer's mental model is wrong. When you wrote the code, you believed it was correct. That belief makes the error invisible: you look at the code and see what you intended, not what is actually written. This is why testing matters so much — running the program against known expected outputs reveals logic errors that careful reading might miss entirely.

Reading Error Messages

Many beginning programmers look at error messages with dread and skip over them. This is a mistake. Error messages contain real, useful information about what went wrong. Learning to read them efficiently is one of the most practical skills in debugging.

Anatomy of a Python Error Message

Typical Python Runtime Error Message
Traceback (most recent call last):
  File "grades.py", line 14, in calculate_average
    average = total / count
ZeroDivisionError: division by zero
Part of MessageWhat It Tells You
Traceback (most recent call last):Python is about to show the sequence of function calls that led to the error
File "grades.py", line 14The file name and line number where the crash occurred
average = total / countThe actual line of code being executed when the error occurred
ZeroDivisionError: division by zeroThe error category and a plain-language description of the problem
Reading Tracebacks

For longer tracebacks with multiple function calls, read from the bottom up. The actual error is always at the bottom. The lines above it show the chain of function calls that led there. Start your investigation at the crash point (bottom), then trace upward only if you need to understand how the program arrived there.

Section 10.6
Debugging Strategies
Seven Reliable Methods for Finding and Fixing Bugs

Debugging Strategies

Reading error messages is the first step. But not every bug produces a helpful error message — logic errors produce none at all — and even with an error message, you may not immediately understand what went wrong. The following strategies form a reliable toolkit for finding and fixing bugs at every level of complexity.

Strategy 1: Read the Code Carefully and Slowly

The simplest debugging strategy is also the most frequently skipped: read the code. Read it slowly, line by line, as if you have never seen it before. Do not read what you intended to write. Read what is actually written. A surprisingly high percentage of bugs can be found this way. Look for wrong variable names (especially when two variables have similar names), wrong operators (> versus >=, and versus or), missing updates inside loops, reversed conditions, and correct actions placed in the wrong location.

Strategy 2: Trace the Program by Hand

Pick up a pencil and paper. Work through the program manually, step by step, exactly as the computer would. Keep a table tracking the value of every important variable at every step. This technique — called hand tracing or desk checking — is powerful because it forces you to follow the actual logic of the code rather than the logic you believe the code has.

Worked Example 10.6.1
Hand tracing to reveal a logic error — finding the largest value
  • Program
    scores = [3, 7, 2, 9, 4]
    largest = 0   # The bug is here
    for score in scores:
        if score > largest:
            largest = score
    print("Largest:", largest)
  • Trace (positive list)
    Stepscorelargestscore > largestUpdate?
    Start—0——
    130Truelargest = 3
    273Truelargest = 7
    327Falseno change
    497Truelargest = 9
    End—9—Output: 9 ✓
  • Edge case trace: all negatives

    Now try with scores = [-5, -3, -8, -1]:

    Stepscorelargestscore > largestUpdate?
    Start—0——
    1-50Falseno change
    2-30Falseno change
    3-80Falseno change
    4-10Falseno change
    End—0—Output: 0 ✗ (should be -1)
  • Bug foundInitializing largest = 0 assumes all values will be positive. The fix: initialize to the first element of the list — largest = scores[0]. This is a logic error that hand tracing reveals clearly. No error message would ever catch it.

Strategy 3: Add Print Statements

One of the most practical debugging tools available at any level is the simple print() function. Add print statements at key points to display the values of variables as the program runs. This makes the computation completely visible. When the bug is fixed, remove or comment out the debugging print statements.

Print Debugging — Making the Computation Visible
def calculate_average(scores):
    total = 0
    count = 0
    for score in scores:
        total = total + score
        count = count + 1
        print(f"  After adding {score}: total={total}, count={count}")
    print(f"Computing: {total} / {count}")
    return total / count

Strategy 4: Simplify the Problem

If a bug is hard to find in a complex program, try to reproduce it in a simpler context. Extract just the suspected function, remove everything else, and test it in isolation with a few simple inputs. If the function works correctly alone, the bug is in how it is called or how input is prepared — not inside the function itself.

Strategy 5: Check Your Assumptions

Most bugs exist because a programmer assumed something that was not true. When stuck, write down the assumptions your code makes and then check each one explicitly.

Common Assumptions Worth Questioning

→ Is the list ever empty?

→ Can any numeric value be zero or negative?

→ Can the user enter unexpected characters or spaces?

→ Does this function always return a value for every possible input?

→ Is this variable always set before it is used?

Strategy 6: Work Backward from Wrong Output

When you have wrong output but no error message, start at the output and work backward. Ask: what value produced this output? What code set that value? What values did that code depend on? Keep tracing backward until you find the step where something went wrong. This reverse-tracing technique is particularly useful for logic errors.

Strategy 7: Take a Break

When you stare at the same code for a long time, your brain fills in what it expects to see rather than what is there. You stop reading the code and start reading your memory of writing the code. Taking a break of fifteen minutes, an hour, or a day — returning fresh — frequently makes previously invisible bugs immediately obvious. If you have been debugging for a long time without success, stopping is often the most efficient path forward.

Sections 10.7 – 10.8
Test Cases and Edge Cases
Designing Deliberate Tests · Boundary and Extreme Inputs

Test Cases

Debugging is reactive: something goes wrong, and you hunt for the cause. Testing is proactive: before or after writing code, you deliberately run it with specific inputs to verify that it produces correct outputs. A test case is a specific input combined with its expected output. You run the program with the input and compare the actual output to the expected output. If they match, that case is handled correctly. If they do not match, there is a bug.

The key discipline is that test cases should be designed before running the program when possible, not after. Deciding what the correct output should be before you see what the program produces forces you to think clearly about the specification. Once you have already seen the output, it is easy to unconsciously rationalize it as correct.

Worked Example 10.7.1
Test cases for a Celsius-to-Fahrenheit converter — formula: (C × 9/5) + 32
#Input (°C)Expected Output (°F)Reasoning
1032.0Freezing point of water — well-known anchor
2100212.0Boiling point of water — well-known anchor
3-40-40.0Only point where Celsius and Fahrenheit are equal
43798.6Normal human body temperature
52068.0Typical room temperature — sanity check
Worked Example 10.7.2
Test cases for a list average function
#InputExpectedReasoning
1[90]90.0Single-element list — should return that element
2[80, 90]85.0Two elements, clean average
3[70, 80, 90]80.0Three elements, symmetric around middle
4[100, 100, 100]100.0All same values
5[0, 0, 0]0.0All zeros
6[75, 82, 91, 68, 95]82.2Typical realistic case
Testing vs. Running

Running a program once and seeing plausible-looking output is not testing — it is hoping. Real testing means deliberately choosing inputs that cover different scenarios and verifying outputs against carefully determined correct values known in advance. One successful run proves nothing about edge cases or boundary conditions.

Edge Cases

Typical test cases verify that a program handles normal inputs correctly. Edge cases test the boundaries, extremes, and special situations that a program might encounter — the cases most likely to reveal hidden bugs and violated assumptions.

Most programs are written with typical inputs in mind. The code that handles typical inputs is usually correct because it is the code the programmer thought about most carefully. Edge cases are the inputs the programmer did not think carefully about — and they often expose assumptions that were wrong.

Edge Case CategoryDescriptionExample
Empty inputEmpty list, string, or filescores = [] passed to find_largest()
Single elementCollection with exactly one itemcalculate_average([88])
Boundary valueExactly at the limit of a conditionScore of exactly 60 in a pass/fail function
Negative valuesValues below zeroList of all negative numbers in a max finder
All same valuesNo variety in input[5, 5, 5, 5] passed to a sorting function
Very large valuesExtreme inputs that might overflow or slow downVery long strings, very large numbers
Worked Example 10.8.1
Edge cases for a grade assignment function — A ≥ 90, B 80–89, C 70–79, D 60–69, F below 60
InputExpectedWhy This Input Matters
95AMiddle of the A range — typical case
90AExact lower boundary of A — tests >= vs >
89BJust below A — tests the boundary from the other side
80BExact lower boundary of B
79CJust below B
70CExact lower boundary of C
60DExact lower boundary of D
59FJust below D
100AMaximum possible — tests the top extreme
0FMinimum — tests the bottom extreme
Testing all of these — not just a "typical" score like 85 — gives real confidence that every boundary is handled correctly.
Sections 10.9 – 10.10
Incremental Development and Program Design
Building Programs in Small Steps · Documentation · Naming · Design Before Coding

Incremental Development

The best time to prevent debugging difficulty is while writing code, not after. Incremental development is a programming approach that builds programs in small, tested pieces rather than writing everything at once and then trying to make it work.

The core principle: write a little, test it, fix any problems, then write a little more. This approach keeps the gap between working code and untested code very small. If a bug appears after you add a new piece, you know the bug is almost certainly in that new piece — not buried somewhere in fifty lines you wrote all at once.

The Alternative — And Why It Fails

Writing the entire program, running it for the first time, and finding it does not work forces you to debug a large untested system where the bug could be anywhere. Every component is suspect. Beginning programmers often feel it is more efficient to write everything before testing — in practice, the debugging time at the end far exceeds the testing time that could have been distributed throughout.

Worked Example 10.9.1
Building a grade statistics program incrementally — five steps
  • Step 1

    Write and test the simplest possible version: read a list and print it.

    scores = [88, 92, 74, 95, 81, 67, 78]
    print("Scores:", scores)

    Does it print correctly? Yes. Move on.

  • Step 2

    Add the calculation of the total.

    scores = [88, 92, 74, 95, 81, 67, 78]
    total = sum(scores)
    print("Total:", total)

    Is the total correct? 88+92+74+95+81+67+78 = 575. Yes. Move on.

  • Step 3

    Add the average calculation.

    total = sum(scores)
    count = len(scores)
    average = total / count
    print("Average:", round(average, 2))

    Is the average correct? 575 / 7 ≈ 82.14. Yes. Move on.

  • Step 4

    Add highest and lowest scores.

    highest = max(scores)
    lowest = min(scores)
    print("Highest:", highest)
    print("Lowest:", lowest)

    Correct? Yes. Move on.

  • Step 5

    Clean up output, remove intermediate prints, finalize.

    If a bug appeared at step 3, it was almost certainly in the code added at step 3 — not buried in everything written since the beginning. Each step was a small, verified addition.

Incremental Development Rules

Start with the simplest version that does something visible. Make each addition as small as possible. Test after every addition. Keep a working version at all times — never write so much untested code that you cannot return to a working state.

Documentation and Program Design

Writing correct code is necessary. Writing code that others — and your future self — can understand is equally important. Documentation and program design are the practices that make programs readable, maintainable, and collaborative.

Forms of Documentation

FormWhat It IsWhere It Appears
CommentsNotes inside code marked with #, ignored by PythonNext to tricky lines, at the top of functions and files
DocstringsString literals below a function definition that describe its purposeFirst line inside every function body
External docsSeparate files, readme documents, or written guidesProject folder, shared team documentation

Writing Good Comments

Example 10.10.1
Useless comment vs. useful comment
Useless — just repeats what the code already says
# Assign 10 to count
count = 10
Useful — explains the reasoning behind the choice
# Start count at 10: the survey skips the first 10 warm-up responses
count = 10

# Guard against zero before dividing to prevent ZeroDivisionError
if count > 0:
    average = total / count

Writing Docstrings

A Well-Documented Function
def calculate_average(scores):
    """
    Calculate and return the arithmetic mean of a list of numbers.

    Parameters:
        scores: a list of numbers (may be empty)

    Returns:
        The arithmetic mean of the values in scores.
        Returns 0.0 if the list is empty.
    """
    if len(scores) == 0:
        return 0.0
    return sum(scores) / len(scores)

Naming as a Form of Design

Poor NameBetter NameWhy
xscoreDescribes what the variable holds
lststudent_namesSpecific about the contents
temptemperature_celsiusIncludes units to prevent confusion
do_thingconvert_to_fahrenheitStates exactly what the function does
flagis_validBoolean names read like true/false questions
ncountDescribes the role, not just a letter

Program Design Questions Before Coding

Ask These Before Writing a Line of Code

1. What is this program supposed to do? (State it clearly in plain language.)

2. What are the inputs? What values, files, or information does it receive?

3. What are the outputs? What should the program produce?

4. What are the major steps? (Write pseudocode or a task list first.)

5. What functions are needed? Which parts of the work are distinct enough to be separated?

6. What can go wrong? Identify edge cases before they become bugs.

Design, Testing, and Debugging Are Connected

Good program design makes testing easier — well-decomposed functions with clear purposes can be tested individually. Good design makes debugging easier — clear names and useful comments make it much faster to understand what each piece of code is supposed to do and to notice when it is not doing it. Design, testing, and debugging are not separate activities. They are three aspects of a single discipline: writing programs that actually work.

Chapter Summary

Key Terms Review

syntax error
An error caused by code that violates the grammar rules of a programming language. Detected before the program runs; prevents execution.
runtime error
An error that occurs during program execution when a syntactically correct operation cannot be completed. Also called an exception.
logic error
A mistake in a program's reasoning. The program runs and produces output, but the output is wrong. No error message is produced.
exception
The technical term for a runtime error in Python. When Python cannot complete an operation, it raises an exception and stops execution.
traceback
The error report Python produces when a runtime error occurs, showing the sequence of function calls that led to the crash. Read from the bottom up.
debugging
The process of finding and fixing errors in a program. Debugging is a skill that improves with practice and systematic strategies.
hand tracing
A debugging technique in which a programmer follows a program's logic step by step on paper, tracking variable values at each step.
print debugging
A technique in which print statements are added to a program to display variable values and confirm execution paths during a run.
test case
A specific input paired with its expected output, used to verify that a program or function behaves correctly.
edge case
An input at the boundary or extreme of a program's expected range — empty collections, exact boundary values, negatives — that often reveals hidden bugs.
off-by-one error
A logic error in which a loop, count, or boundary condition is wrong by exactly one. One of the most common programming mistakes.
incremental development
A programming approach in which code is written in small steps, each tested before the next is added. Keeps bugs small and isolated.
documentation
Written explanations of a program's purpose, design, and behavior. Includes comments inside code, docstrings for functions, and external guides.
comment
A note written inside code using # that the interpreter ignores, used to explain purpose, reasoning, or structure.
docstring
A string literal placed at the start of a function body that serves as documentation: describing the function's purpose, parameters, and return value.
program design
The practice of thinking about structure, naming, decomposition, inputs, outputs, and edge cases before and during the writing of code.

Error Classification Practice

For each description, identify the error type: syntax error, runtime error, or logic error. Explain your answer in one sentence.

  1. A program is supposed to print the numbers from 1 to 5. It prints 0, 1, 2, 3, 4.
  2. Python refuses to run a program and reports SyntaxError: expected ':' on line 6.
  3. A program calculates a shopping discount. When the discount is 100%, it crashes with ZeroDivisionError.
  4. A function is supposed to return the second item in a list. It always returns the first item.
  5. Python reports NameError: name 'total' is not defined when the program runs.
  6. A temperature conversion function produces results that are off by a fixed amount for every input — always 32 degrees too high.
  7. Python refuses to start the program because a closing parenthesis is missing on line 12.
  8. A sorting function crashes on an empty list with IndexError: list index out of range.

Debugging Scenarios

Each program below contains one error. Identify the type of error, explain what it is, and write a corrected version.

Scenario A
def greet(name): message = "Hello, " + name print(message) greet("Amara") greet("Chen) greet("Jordan")

What type of error is this? Where exactly is the mistake? Write the corrected version.

Scenario B
def count_down(n): while n > 0: print(n) print("Done!") count_down(5)

What type of error is this? What is wrong? What happens when you run this program? Write the corrected version.

Scenario C
def find_minimum(values): minimum = 999999 for v in values: if v > minimum: # The bug is on this line minimum = v return minimum

What type of error is this? Trace the function by hand with [4, 1, 8, 2] to show what it actually returns. What is the correct version?

Scenario D
prices = [12.99, 7.50, 22.00, 4.75] total = 0 for price in prices: total = total + price average = total / len(prices) tax_rate = 0.08 total_with_tax = total * tax_rate # Should this be total * (1 + tax_rate)? print("Average price:", round(average, 2)) print("Total with tax:", round(total_with_tax, 2))

Identify the logic error in the total_with_tax calculation. What is the correct formula? What does the current code actually compute?

Test Case Design

For each function described below, design at least four test cases. Give the input and the expected output for each. At least one test case should be an edge case.

  1. A function is_even(n) that returns True if n is even and False if it is odd.
  2. A function clamp(value, minimum, maximum) that returns value if it falls between minimum and maximum, returns minimum if value is below, and returns maximum if value is above.
  3. A function count_vowels(word) that returns the number of vowels (a, e, i, o, u) in a given string.
  4. A function safe_divide(numerator, denominator) that returns the result of division if denominator is not zero, and returns 0.0 if it is zero.

Hand-Tracing Practice

Trace each program by hand. Create a variable-tracking table at each step. Then write the exact output the program produces.

Program 1
total = 0 values = [4, 7, 2, 9, 5] for v in values: if v > 5: total = total + v print("Total:", total)

Trace all five iterations. Track: v, total, condition result. What does the program print?

Program 2
def mystery(n): result = 1 for i in range(1, n + 1): result = result * i return result print(mystery(5))

Trace the function call mystery(5). Track: i, result at each iteration. What does this function compute? What does it print?

Program 3
words = ["apple", "banana", "cherry", "date"] count = 0 for word in words: if len(word) > 5: count = count + 1 print("Long words:", count)

Trace all four iterations. Track: word, len(word), condition, count. What does the program print?

Documentation Practice

  1. The following function has no documentation and poor names. First, add a docstring and appropriate comments. Second, rename all variables and the function so the code is self-explanatory without needing to read the comments.
    def process(lst, t): r = [] for x in lst: if x >= t: r.append(x) return r
  2. Write a docstring for a function send_reminder(recipient, message, days_before) that schedules a reminder email. The function returns True if the reminder was scheduled successfully and False otherwise.
  3. A classmate says: "I never add comments to my code because I know what it does and I can always read it later." Write a response — at least two paragraphs — explaining why this attitude is a problem, using concrete examples.

Incremental Development Practice

A program is supposed to read a list of student names and test scores, calculate the class average, and identify which students scored above the average. Plan how to build this program incrementally.

  1. Describe the first version you would write and test. What would it do? What would its output look like?
  2. Describe the second version — what you would add and test next.
  3. Describe the third version.
  4. Continue until the full program is described. You should have at least four incremental steps total.
  5. For each step, describe what test input you would use and what correct output you would expect.

Edge Case Identification

For each function described below, list at least three edge cases that good tests should cover. For each, explain what might go wrong if the code does not handle it.

  1. A function that finds the second-largest value in a list of numbers.
  2. A function that checks whether a given string is a palindrome (reads the same forward and backward).
  3. A function that removes duplicate values from a list, preserving the order of first occurrence.
  4. A function that takes a sentence as a string and returns the number of words in it.

Chapter 10 Checkpoint

Chapter 10 Checkpoint
Complete without assistance — covers all major skills and concepts from Chapter 10
Section 1 — Vocabulary and Concepts
  1. Name and define the three categories of programming errors. Give one example of each.
  2. What is an exception? In which error category do exceptions appear?
  3. What is the difference between testing a program and running it once?
  4. Explain what an edge case is and why edge cases matter.
Section 2 — Error Identification
  1. Classify each error: (a) NameError: name 'score' is not defined (b) A function that computes total + count instead of total / count (c) SyntaxError: invalid syntax on line 8 (d) IndexError: list index out of range
  2. A student writes if x = 5: instead of if x == 5:. What type of error is this? Why?
Section 3 — Debugging
  1. Trace the following program by hand with numbers = [3, 8, 1, 6, 2]. What does it print?
    total = 0; for n in numbers: total = total + n; print(total / len(numbers))
  2. Name two situations where print debugging would be more useful than reading the code, and explain why.
  3. A program produces wrong output but no error message. List three debugging strategies you would use, in order, and explain why you chose that order.
Section 4 — Test Cases
  1. Design five test cases for a function letter_grade(score) that returns "Pass" for scores 60 and above and "Fail" for scores below 60. Include at least two edge cases.
  2. Why should test cases be designed before running a program, rather than after?
Section 5 — Design and Documentation
  1. Write a useful comment (not a redundant one) for this line: results = [x for x in scores if x >= passing_score]
  2. Write a docstring for a function find_largest(numbers) that returns the largest value in a non-empty list.
  3. A classmate writes an entire 50-line program before testing any of it. What is the risk of this approach? How would you advise them to change their workflow?

Mixed Review

These questions draw on material from Chapters 5 through 10.

  1. What is the difference between a parameter and an argument?
  2. Write a function double_all(numbers) that takes a list of numbers and returns a new list where every value is doubled. Then design three test cases for it, including one edge case.
  3. What does this program print?
    x = 10 y = 3 print(x // y) print(x % y)
  4. Rewrite the following code using a function to eliminate repetition:
    print("Name:", "Alice") print("Score:", 88) print("Grade:", "B") print("---") print("Name:", "Ben") print("Score:", 72) print("Grade:", "C") print("---")
  5. A program is supposed to print all even numbers from 2 to 20. A student writes:
    for i in range(2, 20): if i % 2 == 0: print(i)
    What error does this contain? What is the correct version?
  6. What is the purpose of a return value in a function? Give one example where a return value is essential, and one example where a function that only produces output (with no return value) is appropriate.

Real-World Applications

  1. Software in a hospital manages medication dosing. A logic error causes the program to calculate doses 10% too high. What are the potential consequences? Who bears responsibility — the programmer, the hospital, the software company? What testing requirements should apply to safety-critical medical software?
  2. A navigation app calculates the wrong travel time because it does not handle road closures as an edge case. What other edge cases should the developers have anticipated for a navigation system? What kind of testing would catch these?
  3. An online store's checkout program crashes when a user tries to check out with an empty cart. (a) What type of error is this? (b) What test case would have caught it during development? (c) What line of defensive code would prevent it?
  4. A payroll system at a company has been running for two years. A newly hired programmer modifies the overtime calculation function. After the change, employee paychecks are slightly wrong for employees working more than 40 hours. What debugging strategies would you use? What development practices could have prevented this from reaching production?
  5. Professional software development teams often require that every bug fix include a new test case specifically for the bug that was fixed. This is called a regression test. Explain what regression tests accomplish over time and why they are valuable.
Looking Ahead
Chapter 11: Data Structures at an Introductory Level

Chapter 10 closes Part II of Computer Science Foundations. You have now learned the foundations of programming: algorithms, programming languages, variables and types, control flow, functions, and in this chapter, debugging, testing, and program design. These ideas are the foundation on which everything else in the programming world is built.

Chapter 11 opens Part III: Data and Algorithms. It introduces data structures — the organized containers that programs use to store, organize, and work with collections of data. You have already used one data structure in earlier chapters: the list. Chapter 11 examines lists more carefully and introduces strings as sequences, records, dictionaries, stacks, and queues. Each is designed for a different organizational need. The question that drives Chapter 11 is one that will keep appearing throughout computer science: not just how to process data, but how to organize it so that processing becomes efficient, correct, and clear.

MGU Library Connection
  • Appendix E of this textbook: Debugging and Testing Reference
  • Appendix D of this textbook: Python-Style Code Reference
  • Chapter 9: Functions, Decomposition, and Reuse — functions make incremental development and unit testing possible
  • Chapter 12: Searching, Sorting, and Efficiency — algorithms that require especially careful testing of edge cases
  • Python Programming Foundations, Chapter 3: Debugging and Testing in Practice
  • MGU Dictionary entries: syntax error, runtime error, logic error, exception, test case, edge case, incremental development