FreeCampus Python

Understand Failures and Read Tracebacks

Distinguish syntax failures, runtime exceptions, and wrong results, then use tracebacks and small inspections to find the next useful clue.
python-foundations errors-exceptions-debugging tracebacks exceptions
Open in Colab
  • Level: Python Foundations
  • Estimated time: 4–5 hours
  • 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:

  1. one never starts because a colon is missing;
  2. one starts but stops while adding a score stored as text; and
  3. 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:

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
logic bug execution finishes, but behavior violates a rule wrong output, failed assertion, or user report state expected versus actual and shrink the case

Here is a correct baseline:

scores = [8, 10, 7]
festival_total = sum(scores)
print(festival_total)

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 = 10
if score >= 10
    print("Bonus round!")

A local run may report:

  File "festival.py", line 2
    if score >= 10
                  ^
SyntaxError: expected ':'

Read it in three passes:

  1. SyntaxError names the category.
  2. expected ':' describes what the parser needed.
  3. 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."""
    return sum(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.

Checkpoint: classify before repairing

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 * multiplier


def 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 * multiplier
TypeError: unsupported operand type(s) for *: 'int' and 'NoneType'

Each frame is a paused layer of work:

  1. module code called build_leaderboard;
  2. the comprehension called score_player;
  3. score_player supplied values to apply_bonus;
  4. 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:

suspicious_score = " 10 "
print(repr(suspicious_score))
print(type(suspicious_score).__name__)
print(len(suspicious_score))

Expected output:

' 10 '
str
4

For mappings, inspect available keys rather than assuming one exists:

player = {"name": "Ada", "points": 8}
print(sorted(player.keys()))
print(player.get("score", "<missing>"))

For a collection, inspect shape and representative values:

scores = [8, "10", 7]
print("length:", len(scores))
print("values:", repr(scores))
print("types:", [type(score).__name__ for score in scores])

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.

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:
        return int(raw_score)
    except ValueError as error:
        raise ValueError(
            f"invalid score for player {player_name!r}: {raw_score!r}"
        ) from error


try:
    parse_score("many", "Ada")
except ValueError as 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 + bonus
    return subtotal + bonus


actual = final_score(8, 2)
expected = 10
print("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:

  1. save the notebook;
  2. restart the runtime or kernel;
  3. run all cells from top to bottom;
  4. confirm the original failure and nearby assertions;
  5. 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 + right


try:
    add_scores(8, "10")
except TypeError as error:
    print(error)

Inspect repr and type for both arguments. Decide where conversion belongs if the string came from user input.

Ticket 4: unconvertible score

raw_scores = ["8", "many", "7"]

for position, raw_score in enumerate(raw_scores):
    try:
        print(int(raw_score))
    except ValueError as error:
        print("position:", position, "value:", repr(raw_score))
        print(error)

Identify the exact element and preserve its raw representation.

Ticket 5: absent player

players = ["Ada", "Lin"]

try:
    print(players[2])
except IndexError as error:
    print("length:", len(players))
    print(error)

Explain why the maximum valid index is one when the length is two.

Ticket 6: changed record shape

record = {"player": "Ada", "points": 8}

try:
    print(record["score"])
except KeyError as error:
    print("keys:", sorted(record))
    print(error)

Do not add a default until you decide whether score is optional or the producer changed its field name.

Ticket 7: empty average

def average_score(scores):
    return sum(scores) / len(scores)


try:
    average_score([])
except ZeroDivisionError as error:
    print("empty input reached average_score")
    print(error)

Write the intended empty-input behavior before changing the function.

Ticket 8: silent bonus defect

def award_bonus(score, won_round):
    if won_round:
        score = score + 2
    return score + 2


assert award_bonus(8, False) == 8
assert award_bonus(8, True) == 10

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."""
    if not scores:
        raise ValueError("cannot average an empty score collection")
    return sum(scores) / len(scores)

The double-bonus repair removes the unconditional extra addition:

def award_bonus(score, won_round):
    if won_round:
        score = score + 2
    return score


assert award_bonus(8, False) == 8
assert award_bonus(8, True) == 10

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.

Checkpoint: choose the next investigation move

11. Key points for exception design

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

References and next steps

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.

Back to top