Programming with Python › Python Setup and First Programs › Day 48
Hands-on lab — Day 48: Reading Error Messages and Debugging
- ← Back to the Day 48 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/programming-with-python/day-048-reading-error-messages-and-debugging/
Commands
Setup
cd labs/sections/programming-with-python/day-048-reading-error-messages-and-debugging Run
python3 examples/buggy/average_scores.py
bash examples/debug_walkthrough.sh
python3 starter/average_scores.py Test
bash tests/run_tests.sh File tree
examples/buggy/average_scores.py examples/buggy/lookup_capital.py examples/buggy/total_price.py examples/debug_walkthrough.sh examples/fixed/average_scores.py examples/fixed/lookup_capital.py examples/fixed/total_price.py expected-output/FIELDS.md expected-output/tracebacks.txt expected-output/walkthrough.txt metadata.yml README.md requirements/README.md security.md starter/average_scores.py starter/debug-worksheet.md starter/lookup_capital.py starter/total_price.py tests/run_tests.sh troubleshooting.md
Lab README
Day 048 lab — Read the Traceback, Fix the Bug
Lesson
- Lesson title: Reading Error Messages and Debugging
- Day number: 48 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-048-reading-error-messages-and-debugging
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-048-reading-error-messages-and-debuggingwhen the site is running.
Purpose
Day 48's lesson teaches you to read a Python traceback bottom-up and to debug methodically. This lab makes it concrete: you are handed three tiny Python programs, each of which crashes with a different common exception. You run each one, read its real traceback, diagnose the cause, and fix it — the exact loop you will run thousands of times over your career.
Learning objectives
- Trigger and read three real tracebacks, one each for
IndexError,KeyError, andTypeError. - Identify, from a traceback, the exception type, the culprit line number, and the failing line of code.
- Diagnose the underlying cause of each bug and apply a correct fix.
- Record your reasoning (type, line, cause, fix) in a structured worksheet.
- Run an automated test that confirms the buggy programs fail as expected and the fixed programs succeed.
Prerequisites
- The Day 48 lesson (read it first — it explains how to read a traceback).
- Days 43–47: Python installed and comfort running small programs.
- A terminal and a text editor. No debugging experience required.
Supported operating systems
- macOS — fully supported (tested on macOS with Apple Silicon, Python 3.14.0).
- Linux — fully supported (any distribution with
python3andbash). - Windows — use
pyorpythonin place ofpython3; run the shell scripts inside WSL, or run the Python files directly. The exceptions and line numbers are identical.
Hardware requirements
Any computer that can run Python. The programs are a few lines each and use no meaningful memory, disk, or network.
Required software
python3(3.8 or newer; tested on 3.14.0). Check withpython3 --version.bash(for the walkthrough and test scripts — preinstalled on macOS and Linux).
Free and open-source options
Everything here is free and built in: Python and bash ship with your system or install for free. No account, API key, network access, or purchase is needed.
Installation
None beyond Python itself (installed on Day 43). Clone the repository (or copy this directory) and you are ready:
cd labs/sections/programming-with-python/day-048-reading-error-messages-and-debugging
File structure
day-048-reading-error-messages-and-debugging/
├── README.md ← you are here
├── metadata.yml ← machine-readable lab metadata
├── starter/
│ ├── average_scores.py ← YOUR copy to fix (IndexError)
│ ├── lookup_capital.py ← YOUR copy to fix (KeyError)
│ ├── total_price.py ← YOUR copy to fix (TypeError)
│ └── debug-worksheet.md ← record type, line, cause, and fix
├── examples/
│ ├── buggy/ ← reference broken programs (keep them broken)
│ │ ├── average_scores.py
│ │ ├── lookup_capital.py
│ │ └── total_price.py
│ ├── fixed/ ← reference working versions
│ │ ├── average_scores.py
│ │ ├── lookup_capital.py
│ │ └── total_price.py
│ └── debug_walkthrough.sh ← reads each traceback, then runs the fixes
├── tests/
│ └── run_tests.sh ← automated checks
├── expected-output/
│ ├── tracebacks.txt ← the three real tracebacks
│ ├── walkthrough.txt ← full walkthrough output
│ └── FIELDS.md ← what must be true on every platform
├── requirements/
│ └── README.md ← dependency statement (python3 only)
├── troubleshooting.md
└── security.md
How to run
From this directory:
## 1. See a real traceback with your own eyes
python3 examples/buggy/average_scores.py
## 2. Watch the guided walkthrough read all three, then run the fixes
bash examples/debug_walkthrough.sh
## 3. Your task: fix the three programs in starter/, filling in the worksheet
python3 starter/average_scores.py # read the traceback, then edit the file
python3 starter/lookup_capital.py
python3 starter/total_price.py
## 4. Check your work (and confirm the samples still behave)
bash tests/run_tests.sh
What the commands do
python3 examples/buggy/average_scores.py— runs a deliberately broken program so you see a genuine traceback. It exits non-zero with anIndexError.bash examples/debug_walkthrough.sh— runs all three buggy programs, extracts each exception type and culprit line (reading the traceback bottom-up), then runs the three fixed versions to show they succeed.python3 starter/<name>.py— runs your copy; read the traceback, edit the line markedBUG, and re-run until it succeeds.bash tests/run_tests.sh— verifies each buggy program raises its expected exception (non-zero exit and the exception name in stderr) and each fixed program runs cleanly (exit 0). Exits 0 on success.
Expected output
The three buggy programs produce these exceptions (see expected-output/tracebacks.txt for the full tracebacks):
examples/buggy/average_scores.py → IndexError: list index out of range (line 5)
examples/buggy/lookup_capital.py → KeyError: 'Germany' (line 6)
examples/buggy/total_price.py → TypeError: can only concatenate str (not "int") to str (line 4)
The walkthrough's full output is captured in expected-output/walkthrough.txt. The File "..." path in a traceback shows the path you ran on your own machine; only that wording differs from the captures.
Validation steps
- Run each buggy program and confirm it crashes with the exception above.
- For each program, fill in the matching row of
starter/debug-worksheet.md(type, line, cause, fix). - Edit each starter program so it runs without raising an exception.
- Run the tests (next section) — all checks must pass.
Tests
bash tests/run_tests.sh
Expected final line: 9 checks, 0 failure(s). (three buggy programs checked for the right exception, three fixed programs checked for a clean exit). The command exits 0 on success and non-zero on any failure, so it can run in CI. It makes no network calls.
Cleanup
Nothing to clean up: the programs write no files and make no network calls. To reset your work and the reference samples, restore them from git:
git checkout -- starter examples/buggy
Troubleshooting
See troubleshooting.md for the full list (python3 not found, a fix revealing a second bug, editing the wrong copy, locating the error line).
Security notes
See security.md. Short version: never run untrusted .py files; these samples are tiny, local, read no input, touch no network, and are safe to read and run.
Extension exercises
- Add
breakpoint()before the loop in a broken copy ofaverage_scores.py, run it, and usep scores,p i, andnto watch the index walk off the end. - Fix
lookup_capital.pya different way — add"Germany"to the dictionary — and decide which fix is better for which situation. - Write a fourth buggy program that raises a
ValueError(for exampleint("hello")), predict the traceback, then run it to check.
Navigation
- Previous day: Day 47 — Input, Output, and f-strings (
labs/sections/programming-with-python/day-047-input-output-and-f-strings/). - Next day: Day 49 — Your First Real Program (
labs/sections/programming-with-python/day-049-your-first-real-program/).
Expected output
FIELDS.md
# Expected output — Day 048 lab
Two captures live in this directory, both from real runs on the authoring
machine (macOS, Python 3.14.0, 2026-07-12):
- `tracebacks.txt` — the three genuine tracebacks the buggy programs produce,
one per exception type (IndexError, KeyError, TypeError). The `File "..."`
path is shown in relative form; on any machine only the path wording
changes, never the exception, message, or line number.
- `walkthrough.txt` — the full output of `bash examples/debug_walkthrough.sh`,
which reads each traceback bottom-up (exception type + culprit line) and
then runs the fixed versions to show they succeed.
## What must be true on every platform
1. `examples/buggy/average_scores.py` fails with `IndexError: list index out of range` on line 5.
2. `examples/buggy/lookup_capital.py` fails with `KeyError: 'Germany'` on line 6.
3. `examples/buggy/total_price.py` fails with `TypeError: can only concatenate str (not "int") to str` on line 4.
4. All three `examples/fixed/*.py` programs run to completion and exit 0.
5. `bash tests/run_tests.sh` prints `9 checks, 0 failure(s).` and exits 0.
## Platform and version notes
- The caret/underline marks (`~~~^^^`) under the failing sub-expression appear
on Python 3.11 and newer; on older versions the traceback omits them but is
otherwise the same.
- On Linux the behavior is identical; only the resolved file path differs.
- On Windows, use `py` or `python` in place of `python3`; the exceptions and
line numbers are unchanged.
tracebacks.txt
Real tracebacks captured on the authoring machine (macOS, Python 3.14.0),
run from the lab directory. The path in each `File "..."` line is shown here
in its relative form; on YOUR machine that line shows the path you actually
ran (for example the absolute path Python resolves it to). Only the path
wording differs — the line number, code line, caret marks, and exception are
what matter, and they are identical everywhere.
$ python3 examples/buggy/average_scores.py
Student 1: 88
Student 2: 92
Student 3: 79
Student 4: 95
Traceback (most recent call last):
File "examples/buggy/average_scores.py", line 5, in <module>
print(f"Student {i + 1}: {scores[i]}")
~~~~~~^^^
IndexError: list index out of range
$ python3 examples/buggy/lookup_capital.py
Traceback (most recent call last):
File "examples/buggy/lookup_capital.py", line 6, in <module>
print(f"The capital of {country} is {capitals[country]}.")
~~~~~~~~^^^^^^^^^
KeyError: 'Germany'
$ python3 examples/buggy/total_price.py
Traceback (most recent call last):
File "examples/buggy/total_price.py", line 4, in <module>
total = price + tax
~~~~~~^~~~~
TypeError: can only concatenate str (not "int") to str
Note on Python versions: the caret/underline marks under the offending
sub-expression (the `~~~^^^` lines) appear on Python 3.11 and newer. On older
Python they are absent, but the exception type, message, and line number are
the same.
walkthrough.txt
########################################################
# Reading three real tracebacks (bottom-up) #
########################################################
=== average_scores.py ===
Exception: IndexError: list index out of range
Culprit line 5: print(f"Student {i + 1}: {scores[i]}")
=== lookup_capital.py ===
Exception: KeyError: 'Germany'
Culprit line 6: print(f"The capital of {country} is {capitals[country]}.")
=== total_price.py ===
Exception: TypeError: can only concatenate str (not "int") to str
Culprit line 4: total = price + tax
All three buggy programs failed as expected. Now run the fixed versions:
--- fixed/average_scores.py ---
Student 1: 88
Student 2: 92
Student 3: 79
Student 4: 95
Average: 88.5
--- fixed/lookup_capital.py ---
The capital of Germany is unknown.
--- fixed/total_price.py ---
Total: 12
Each fixed program ran cleanly (exit 0). That is the whole loop:
reproduce -> read -> isolate -> hypothesize -> test -> fix.
Source files
examples/buggy/average_scores.py (292 bytes)
# average_scores.py — prints each student's score, then the average.
scores = [88, 92, 79, 95]
for i in range(len(scores) + 1): # BUG: + 1 makes the loop walk past the last index
print(f"Student {i + 1}: {scores[i]}")
average = sum(scores) / len(scores)
print(f"Average: {average}")
examples/buggy/lookup_capital.py (258 bytes)
# lookup_capital.py — looks up a country's capital city.
capitals = {"France": "Paris", "Japan": "Tokyo", "Kenya": "Nairobi"}
country = "Germany" # BUG: "Germany" is not a key in the dictionary
print(f"The capital of {country} is {capitals[country]}.")
examples/buggy/total_price.py (185 bytes)
# total_price.py — adds tax to a price and prints the total.
price = "10" # BUG: this is a string, but arithmetic needs a number
tax = 2
total = price + tax
print(f"Total: {total}")
examples/debug_walkthrough.sh (1921 bytes)
#!/usr/bin/env bash
# Day 048 lab — guided traceback walkthrough.
#
# Runs each buggy program, captures its traceback, and points out the
# exception type and the culprit line — reading the traceback the way the
# lesson teaches (bottom-up). Then runs the FIXED versions to show they work.
#
# Run from the lab directory:
# bash examples/debug_walkthrough.sh
set -u
# Resolve directories relative to this script, so it works from anywhere.
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
buggy_dir="${here}/buggy"
fixed_dir="${here}/fixed"
# Pick an available Python interpreter.
PY="python3"
command -v "${PY}" >/dev/null 2>&1 || PY="python"
programs="average_scores lookup_capital total_price"
echo "########################################################"
echo "# Reading three real tracebacks (bottom-up) #"
echo "########################################################"
echo
for name in ${programs}; do
src="${buggy_dir}/${name}.py"
err="$(mktemp)"
# Run the buggy program; it is expected to crash. Capture stderr.
"${PY}" "${src}" >/dev/null 2>"${err}"
# The LAST line of a traceback is the exception type and message (the "what").
exception="$(tail -n 1 "${err}")"
# The LAST "File ... line N" frame is the culprit (the "where").
line_no="$(grep -E '^ File ' "${err}" | tail -n 1 | sed -E 's/.*line ([0-9]+),.*/\1/')"
culprit="$(sed -n "${line_no}p" "${src}" | sed -E 's/^[[:space:]]+//')"
echo "=== ${name}.py ==="
echo "Exception: ${exception}"
echo "Culprit line ${line_no}: ${culprit}"
echo
rm -f "${err}"
done
echo "All three buggy programs failed as expected. Now run the fixed versions:"
echo
for name in ${programs}; do
echo "--- fixed/${name}.py ---"
"${PY}" "${fixed_dir}/${name}.py"
echo
done
echo "Each fixed program ran cleanly (exit 0). That is the whole loop:"
echo "reproduce -> read -> isolate -> hypothesize -> test -> fix."
examples/fixed/average_scores.py (294 bytes)
# average_scores.py — FIXED: the loop now stops at the last valid index.
scores = [88, 92, 79, 95]
for i in range(len(scores)): # FIX: range(len(scores)) yields 0..len-1, all valid
print(f"Student {i + 1}: {scores[i]}")
average = sum(scores) / len(scores)
print(f"Average: {average}")
examples/fixed/lookup_capital.py (400 bytes)
# lookup_capital.py — FIXED: .get() returns a default instead of raising KeyError.
capitals = {"France": "Paris", "Japan": "Tokyo", "Kenya": "Nairobi"}
country = "Germany" # still not in the dictionary — but now we handle that
# FIX: dict.get(key, default) returns the default when the key is missing
capital = capitals.get(country, "unknown")
print(f"The capital of {country} is {capital}.")
examples/fixed/total_price.py (176 bytes)
# total_price.py — FIXED: price is now a number, so + means addition.
price = 10 # FIX: an integer, not the string "10"
tax = 2
total = price + tax
print(f"Total: {total}")
metadata.yml (661 bytes)
lesson_id: D048
day: 48
kind: python-program
languages: [python]
setup_commands:
- cd labs/sections/programming-with-python/day-048-reading-error-messages-and-debugging
run_commands:
- python3 examples/buggy/average_scores.py
- bash examples/debug_walkthrough.sh
- python3 starter/average_scores.py
test_commands:
- bash tests/run_tests.sh
cleanup_commands:
- 'git checkout -- starter examples/buggy # optional: reset your work and the samples'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (Apple Silicon), Python 3.14.0, bash tests/run_tests.sh → 9 checks, 0 failure(s).'
requirements/README.md (606 bytes)
# Dependencies — Day 048 lab
**Only Python 3 and a shell.** This lab has no installable dependencies and no
`requirements.txt`:
- `python3` ≥ 3.8 (installed on Day 43; tested on Python 3.14.0). Check with
`python3 --version`. On Windows use `py` or `python`.
- `bash` (for `examples/debug_walkthrough.sh` and `tests/run_tests.sh`) —
preinstalled on macOS and Linux; available on Windows through WSL or Git Bash.
No third-party packages are imported: every program uses only Python built-ins
(lists, dictionaries, f-strings, `sum`, `len`). Nothing here reaches the
network or needs an API key.
starter/average_scores.py (436 bytes)
# average_scores.py — YOUR COPY TO FIX.
# Run it (python3 average_scores.py), read the traceback bottom-up, then fix
# the one line marked BUG so the program runs cleanly. Record your work in
# debug-worksheet.md.
scores = [88, 92, 79, 95]
for i in range(len(scores) + 1): # BUG: read the traceback, then correct this line
print(f"Student {i + 1}: {scores[i]}")
average = sum(scores) / len(scores)
print(f"Average: {average}")
starter/debug-worksheet.md (996 bytes)
# Debug worksheet — Day 048
Fill this in as you fix each program in `starter/`. Run each one with
`python3 <file>.py`, read the traceback **bottom-up**, then complete the row.
## For each program
| Program | Exception type | Line number | Cause (why the value was wrong) | Your fix |
| --- | --- | --- | --- | --- |
| `average_scores.py` | | | | |
| `lookup_capital.py` | | | | |
| `total_price.py` | | | | |
Example of a completed row (do not copy — investigate for yourself):
| Program | Exception type | Line number | Cause | Your fix |
| --- | --- | --- | --- | --- |
| `demo.py` | `ZeroDivisionError: division by zero` | 4 | `values` was an empty list, so `len(values)` was 0 | guard the empty case before dividing |
## How the three bugs differ
Write 4–6 sentences below: what category of mistake each bug represents (a
bad index, a missing key, a type mismatch), and how the exception type alone
told you which kind of fix each one needed.
_Your paragraph here._
starter/lookup_capital.py (386 bytes)
# lookup_capital.py — YOUR COPY TO FIX.
# Run it, read the traceback bottom-up, then fix the program so it runs
# cleanly (one good fix: use capitals.get(country, "unknown")).
capitals = {"France": "Paris", "Japan": "Tokyo", "Kenya": "Nairobi"}
country = "Germany" # BUG: this key is missing — decide how to handle that
print(f"The capital of {country} is {capitals[country]}.")
starter/total_price.py (269 bytes)
# total_price.py — YOUR COPY TO FIX.
# Run it, read the traceback bottom-up, then fix the program so it runs
# cleanly and prints the numeric total.
price = "10" # BUG: read the traceback, then make this a number
tax = 2
total = price + tax
print(f"Total: {total}")
tests/run_tests.sh (1949 bytes)
#!/usr/bin/env bash
# Tests for the Day 048 lab. Run from the lab directory:
# bash tests/run_tests.sh
#
# Verifies two things, with no network access:
# 1. Each BUGGY program fails with the EXPECTED exception type
# (non-zero exit AND the exception name printed to stderr).
# 2. Each FIXED program runs cleanly (exit 0).
set -u
lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
failures=0
checks=0
PY="python3"
command -v "${PY}" >/dev/null 2>&1 || PY="python"
check() {
local label="$1" ok="$2"
checks=$((checks + 1))
if [ "${ok}" = "yes" ]; then
echo " ok: ${label}"
else
echo " FAIL: ${label}"
failures=$((failures + 1))
fi
}
# Expected exception type for each buggy program.
expect_buggy() {
local name="$1" want="$2"
local src="${lab_dir}/examples/buggy/${name}.py"
local err rc
err="$("${PY}" "${src}" 2>&1 >/dev/null)"
rc=$?
if [ "${rc}" -eq 0 ]; then
check "buggy/${name}.py fails (non-zero exit)" "no"
else
check "buggy/${name}.py fails (non-zero exit)" "yes"
fi
if printf '%s\n' "${err}" | grep -q "${want}"; then
check "buggy/${name}.py raises ${want}" "yes"
else
check "buggy/${name}.py raises ${want}" "no"
printf '%s\n' "${err}" | sed 's/^/ /'
fi
}
# Fixed programs must run cleanly.
expect_fixed_ok() {
local name="$1"
local src="${lab_dir}/examples/fixed/${name}.py"
if "${PY}" "${src}" >/dev/null 2>&1; then
check "fixed/${name}.py runs cleanly (exit 0)" "yes"
else
check "fixed/${name}.py runs cleanly (exit 0)" "no"
fi
}
echo "Checking buggy programs raise the expected exceptions ..."
expect_buggy average_scores IndexError
expect_buggy lookup_capital KeyError
expect_buggy total_price TypeError
echo "Checking fixed programs run cleanly ..."
expect_fixed_ok average_scores
expect_fixed_ok lookup_capital
expect_fixed_ok total_price
echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]
Troubleshooting
Troubleshooting — Day 048 lab
python3: command not found
Python may be installed as python on your system, or not installed at all.
Try python examples/buggy/average_scores.py, and confirm your version with
python3 --version or python --version. If neither works, revisit Day 43
(installing Python). On Windows, use py or python.
Reading a traceback: which line is the error on?
Read the traceback from the bottom up:
- The last line names the exception type and message — the what
(for example
KeyError: 'Germany'). - The bottom-most
File "..."line names the file and the line number — the where. The line printed just beneath it is that exact line of code.
Everything above the bottom frame is the chain of calls that led there; you only need it when the cause hides in a function that called the failing one.
The error line vs the real cause
The line named in a traceback is where the program broke, which is not
always where it went wrong. In average_scores.py the crash is on the
print(...) line, but the real mistake is the loop range on the line above it:
range(len(scores) + 1) counts one position too far. Fix the cause (the
range), not the symptom (the print). When a value is wrong, ask where it was
set — often several lines, or a whole function, earlier.
"My fixed program still crashes"
Read the new traceback. A fix can reveal a second bug that the first crash was hiding, or introduce a new one. That is normal — run the debugging loop again on the new error.
The test script says a buggy program "did not fail"
You probably edited a file inside examples/buggy/ by mistake; those must stay
broken so the walkthrough and tests work. Do your fixing in starter/ only.
Restore the reference samples with git checkout -- examples/buggy.
Permission denied running a script
Run scripts through their interpreter explicitly: bash tests/run_tests.sh,
python3 starter/total_price.py. You do not need to chmod +x anything.
The caret marks (~~~^^^) are missing from my traceback
Those under-line marks pointing at the failing sub-expression appear on Python 3.11 and newer. On older Python they are absent, but the exception type, message, and line number are the same — which is all you need.
Windows: bash is not recognized
Run the Python files directly (python starter\average_scores.py), or use WSL
(wsl --install, then follow the Linux path), or Git Bash for the .sh
scripts.
Security notes
Security notes — Day 048 lab
- Never run untrusted
.pyfiles. A Python program can do anything your user account can do — read and delete files, make network connections, install software. Running a script you have not read is one of the most common ways developers get compromised. The rule this course follows: every script in a lab is small enough to open and read before you run it. Do the same with any code you find elsewhere. - These samples are safe and local. The six programs here are a few lines each. They only build small lists and dictionaries, do arithmetic, and print text. They read no input, open no files, make no network calls, and need no elevated privileges. Open them and confirm this for yourself — that habit is the point.
- The shell scripts are equally small.
debug_walkthrough.shandrun_tests.shonly run the sample programs and read their output. They write nothing outside their own console output (aside from a temporary file the test cleans up) and touch no network. - Tracebacks can leak information. A real traceback shows file paths and internal details. That is fine on your own machine, but when you build programs for others, do not display raw tracebacks to users — log them privately instead, as the lesson explains. The captures in this lab use relative paths so they carry no personal information.