You will learn: Classify failures, read traceback frames from the final line upward, inspect suspicious state, and distinguish a reported location from an earlier cause.
Practice in: Google Colab, JupyterLab, or a local editor
A community festival uses a small program to total game scores. During setup, three versions arrive at the repair desk:
one never starts because a colon is missing;
one starts but stops while adding a score stored as text; and
one prints a total that is too large but reports no error.
The first useful question is not “Which line should I change?” It is “What kind of failure do I have, and what evidence comes with it?” Keep these investigation questions beside your notebook:
Did Python begin running the program?
If execution stopped, what are the exception type and message?
Which call-stack frames belong to code I control?
Which values and types were present at the deepest useful frame?
If no exception occurred, what independent rule proves the result is wrong?
1. Three failures leave three kinds of evidence
Python source passes through parsing before its statements can run. After it starts, an operation may raise an exception. Even when every operation succeeds, the algorithm may still produce the wrong answer.
Category
What happens
Typical evidence
First useful move
syntax failure
Python cannot parse the instructions
SyntaxError or IndentationError, a line, and sometimes a caret
inspect nearby punctuation and block structure
runtime exception
execution starts, then an operation cannot continue
traceback, exception type, and message
read the final line, then inspect frames and values
Python creates a list of three integers, passes it to sum, stores 25, and prints 25. That normal path gives you something to compare with each failure.
The failure category determines which evidence is available before you edit.
flowchart TD
start["Run the smallest known case"] --> began{"Did execution begin?"}
began -->|no| syntax["Read parser location and nearby structure"]
began -->|yes| stopped{"Did an exception stop it?"}
stopped -->|yes| trace["Read final line, then useful frames"]
stopped -->|no| oracle{"Does behavior match the oracle?"}
oracle -->|no| logic["Compare expected and actual state"]
oracle -->|yes| normal["Observed case passes"]
An oracle is any independent statement of expected behavior: an acceptance example, calculation, assertion, specification, or trusted comparison.
2. Syntax failures happen before ordinary execution
This intended program should announce a bonus for a score of at least ten. The missing colon prevents Python from forming the if statement:
score =10if score >=10print("Bonus round!")
A local run may report:
File "festival.py", line 2 if score >= 10 ^SyntaxError: expected ':'
Read it in three passes:
SyntaxError names the category.
expected ':' describes what the parser needed.
The caret points near where parsing became impossible.
The caret is a clue, not a guarantee that the root cause is exactly underneath it. An unclosed parenthesis on an earlier line can make a later, innocent line look suspicious. Inspect the reported line and a few lines above it.
Indentation participates in Python’s grammar. The next program has a colon, but the intended body is not indented:
players = ["Ada", "Lin"]for player in players:print(player)
Python reports an IndentationError, which is a specialized syntax failure. The repair is to make block membership visible:
players = ["Ada", "Lin"]for player in players:print(player)
Do not wrap syntax failures in try/except. The file must be parsed before normal exception handling can run. Repair the source structure first.
TipAsk what Python was trying to finish
For a parser failure, scan for a missing colon, quote, closing bracket, comma, or indented body. Then run again. One syntax repair can reveal the next parser failure that was previously unreachable.
3. Runtime exceptions stop one path through valid code
This program is valid Python, so execution begins:
def total_scores(values):"""Return the sum of numeric scores."""returnsum(values)scores = [8, "10", 7]print(total_scores(scores))
The list mixes integers with the string "10". A terminal traceback resembles:
Traceback (most recent call last): File "festival.py", line 7, in <module> print(total_scores(scores)) ^^^^^^^^^^^^^^^^^^^^ File "festival.py", line 3, in total_scores return sum(values) ^^^^^^^^^^^TypeError: unsupported operand type(s) for +: 'int' and 'str'
Start at the bottom, not at the first red line:
type:TypeError says an operation received incompatible types;
message:int and str were combined with addition;
deepest frame in our code:return sum(values) is where Python noticed;
caller frame:total_scores(scores) shows how execution arrived there.
The failing line is not necessarily the line that introduced the defect. sum correctly rejects the mixed list. The suspicious data was created earlier when "10" entered scores. Tracebacks tell you where execution could not continue; your investigation follows data backward to where the broken assumption began.
4. Traceback frames are a route through function calls
More than one frame is useful when functions call other functions:
def apply_bonus(score, multiplier):"""Return a score after applying a numeric multiplier."""return score * multiplierdef score_player(player):"""Return one player's adjusted score."""return apply_bonus(player["score"], player["bonus"])def build_leaderboard(players):"""Return adjusted scores in input order."""return [score_player(player) for player in players]players = [ {"name": "Ada", "score": 8, "bonus": 2}, {"name": "Lin", "score": 7, "bonus": None},]build_leaderboard(players)
The second player eventually triggers:
Traceback (most recent call last): File "leaderboard.py", line 20, in <module> build_leaderboard(players) File "leaderboard.py", line 13, in build_leaderboard return [score_player(player) for player in players] File "leaderboard.py", line 8, in score_player return apply_bonus(player["score"], player["bonus"]) File "leaderboard.py", line 3, in apply_bonus return score * multiplierTypeError: unsupported operand type(s) for *: 'int' and 'NoneType'
Each frame is a paused layer of work:
module code called build_leaderboard;
the comprehension called score_player;
score_player supplied values to apply_bonus;
multiplication discovered 7 * None could not proceed.
The deepest frame is often the most immediate failure. Move upward to learn who supplied its arguments and which application-level rule was violated. Skip deep library internals at first unless your own frames do not explain the inputs.
5. Inspect values without changing the case
Before repairing, make the suspicious values visible. repr is useful because quotes, escape characters, empty strings, and spaces remain visible:
Temporary observations should answer a question. Printing twenty unrelated variables creates noise. A targeted observation such as “What are the type and representation of the score passed to sum?” can confirm or disprove a hypothesis.
WarningPreserve the reproduction while observing it
Do not call .strip(), replace a missing value, or convert a type merely to print it. That changes the case. First display the state as it exists; repair it only after the evidence supports a cause.
6. Exception names narrow the search
An exception name does not automatically tell you the repair, but it suggests which relationship to inspect.
Inspect both types and the domain rule. The repair might be conversion at an input boundary, or it might be rejecting text instead of silently converting it.
ValueError: the type is acceptable, the value is not
raw_score ="many"try: score =int(raw_score)exceptValueErroras error:print(repr(raw_score), "cannot become an integer")print(error)
The string type is a supported input for int, but these characters do not represent an integer.
IndexError and KeyError: a requested position or key is absent
Ask whether absence is a normal case. If it is, design that behavior explicitly. If the key is required by the contract, a default may hide corrupt data.
AttributeError: the object lacks the requested attribute
name ="Ada"try:print(name.apend("!"))exceptAttributeErroras error:print(type(name).__name__)print(error)
Inspect the object type and spelling. Methods such as append belong to lists, and this example also misspells the intended method.
7. Chained tracebacks preserve two levels of truth
Programs sometimes add domain context to a lower-level exception. The following function explains which festival field failed while preserving int’s original reason:
def parse_score(raw_score, player_name):"""Return an integer score or raise a contextual ValueError."""try:returnint(raw_score)exceptValueErroras error:raiseValueError(f"invalid score for player {player_name!r}: {raw_score!r}" ) from errortry: parse_score("many", "Ada")exceptValueErroras error:print(error)print("original cause:", error.__cause__)
The resulting traceback contains the original conversion failure and the new festival-specific failure, connected by “the above exception was the direct cause.” Do not stop at the first final line when a traceback is chained. Read each exception and the relationship between them. Lesson 2 develops this design in depth.
8. Logic bugs need an oracle because Python sees valid operations
This bonus function runs successfully but awards the bonus twice:
def final_score(base_score, bonus):"""Return the base score plus one bonus.""" subtotal = base_score + bonusreturn subtotal + bonusactual = final_score(8, 2)expected =10print("expected:", expected)print("actual:", actual)assert actual == expected
All values and operations are valid. Python has no way to infer that “one bonus” is the festival rule. The assertion turns a silent wrong result into precise evidence.
Trace the state:
Step
base_score
bonus
subtotal
returned
expected rule
8
2
10
10
actual code
8
2
10
12
The first divergence is the return expression, not the input and not the subtotal. That is a stronger localization than “something in final_score is wrong.” Lesson 3 turns first-divergence searches into a repeatable method.
9. Notebook state can create a fourth kind of confusion
Notebook cells can be run out of order. If you change a function cell but do not rerun it, later cells still use the old function object. If you delete a variable assignment from a cell, the old variable can remain in memory.
Use this verification routine after a repair:
save the notebook;
restart the runtime or kernel;
run all cells from top to bottom;
confirm the original failure and nearby assertions;
record the result only after that clean run.
A clean rerun separates program behavior from accidental session history.
10. Repair the festival scoreboard tickets
Create an evidence table with one row per ticket. For each, record the category, decisive clue, one inspection, root cause, repair, and regression check.
Ticket 1: parser failure
def announce_winner(name)print(f"Winner: {name}")
Run or read the reported syntax failure, repair it, and explain why no function call could run before the repair.
Ticket 2: indentation failure
for score in [8, 10, 7]:print(score)
Repair the block and predict the three output lines before running.
Ticket 3: incompatible score type
def add_scores(left, right):return left + righttry: add_scores(8, "10")exceptTypeErroras error:print(error)
Inspect repr and type for both arguments. Decide where conversion belongs if the string came from user input.
Use the two cases to identify the first rule violation. Repair the cause and keep both assertions as regression checks.
Compare a repair path after investigating all eight tickets
Tickets 1 and 2 need a colon and an indented block before execution can begin. Tickets 3–7 need a contract decision as well as a technical fix: convert scores at the input boundary, decide whether absent positions/keys are expected, and define what an empty average means. For example, an explicit empty-input policy can be:
def average_score(scores):"""Return the mean score; reject an empty score collection."""ifnot scores:raiseValueError("cannot average an empty score collection")returnsum(scores) /len(scores)
The double-bonus repair removes the unconditional extra addition:
Compare the evidence that led you there. A matching final function without a category, first clue, and root-cause explanation is an incomplete repair record.
Syntax failures happen while parsing; inspect structure before ordinary execution.
For a runtime traceback, read the final exception type and message, then move through frames from the deepest useful application frame toward its callers.
The reported line shows where Python noticed a broken assumption, not always where the bad value originated.
repr, type, lengths, keys, arguments, and small state traces turn guesses into evidence.
Exception names narrow the relationship to inspect; they do not choose the product rule for you.
A logic bug requires an oracle and an expected-versus-actual comparison.
A clean notebook restart is part of verification, not optional housekeeping.
Next, you will decide when your own functions should raise an exception, where an application can handle it responsibly, and how to preserve the original cause while adding useful context.