Skip to content
  • Home
  • How To
  • M0
  • Module 1 - Python
  • Agents
  • M2
  • M3
  • M4
  • M5
  • M6
  • Progress
  • Contact
  • Pricing
  • Sign in
  • Future
  1. Module 1M1
  2. ›
    S6Errors and Debugging
  3. ›
    Lesson 1 of 3The Three Kinds of Error and How to Read a Traceback
  4. ›
    S6›Lesson 1 of 3
  1. Module 1
  2. ›
  3. Section 6
  4. ›
  5. Lesson 1

The Three Kinds of Error and How to Read a Traceback

FREE·~1.5 h·2 labs·4 questions·1 assignment


Context

Things go wrong in three distinct ways. Code can be malformed enough that it never runs at all, code can run and then hit something it cannot do and stop, or code can run to completion and produce the wrong answer. When the second happens Python prints a report of where it was and how it got there.

Reading that report is the skill this lesson exists for. A pipeline that dies on row nine hundred of a file names the failing operation and the chain of calls that reached it, and that chain is what turns a vague failure into a specific line. The third kind never prints anything, which is why a metric that looks plausible still has to be checked against a case whose answer you already know.

Skipping to the last line of a failure report and guessing is how hours disappear. The operation that stopped is often not the mistake, it is where a bad value that was created several steps earlier finally met something that refused it, and the chain of calls is where the trail back to it starts. The previous section moved data in and out of files, which is exactly where malformed input first meets your code.


Goal

  • Distinguish syntax errors, runtime errors, and logic errors by their symptoms and timing.
  • Read a traceback from bottom to top to locate the exact line and call that failed.
  • Name the exception type involved in a failure and state what category of problem it signals.
  • Identify which stream a traceback arrives on and why that matters for capturing it.
  • Trace a multi-level traceback through several function calls back to the originating line.

Prerequisites

Optional, neither is a gate. The lesson stands on its own, they are here if you want to see a raw traceback before reading about one.

  • YouTube - "Python Tutorial: Using Try/Except Blocks for Error Handling" by Corey Schafer, the opening minutes that show an unhandled traceback before any handling is added.
  • Official docs - The Python Tutorial, "Syntax Errors" and "Exceptions". Read both, they define the first two categories precisely.

Table of Contents

  • Syntax Error
  • Runtime Error / Exception
  • Logic Error
  • Traceback
  • Exception Type

Theory

Real datasets are full of surprises that crash naive code, and learning to read the traceback is the skill that turns a wall of text into a precise address. This lesson is diagnosis before treatment: before you can handle or fix an error, you must be able to name what kind it is and find exactly where it happened. The loader from the previous lesson is the perfect specimen, because a single malformed row is enough to crash it or, worse, to let it finish with a wrong answer.

Syntax Error

What. A syntax error is code that cannot be parsed at all. Python reads the whole file before running any of it, and if the structure is invalid, it refuses to run a single line. Why. A syntax error in the file you run is the cheapest error to have, because it is caught before anything executes, no data is processed, no files are touched, nothing half-happens. Recognizing it instantly saves you from looking for a runtime cause that does not exist, the failure is purely in how the code is written. When. You meet a syntax error the moment you try to run a file with a typo in its structure: a missing colon, an unclosed bracket, a bad indent. For the file you run, it is always a "before the program starts" failure, never a "while running" one.

A syntax error in the file you run stops the program before execution begins. The classic causes are a missing colon after a compound statement, an unclosed parenthesis or bracket, and inconsistent indentation.

# Missing colon after the 'for' header -> SyntaxError, nothing runs.
for row in records  # <-- no colon
    print(row)

Nothing in this file runs, not even the lines above the mistake, because parsing finishes before execution begins.

A particularly common variant is IndentationError, a subclass (a more specific kind) of syntax error, raised when indentation is inconsistent. Python uses indentation to define blocks, so misaligned lines are a structural failure.

# Inconsistent indentation -> IndentationError, a kind of SyntaxError.
def clean(value):
    value = value.strip()
      return value.lower()  # <-- over-indented, does not line up

Indentation is part of Python's grammar rather than a matter of style, which is why a misaligned line is a parse failure and not a warning.

The distinguishing mark of a syntax error in the file you run is its timing: it appears immediately, with no output from the program at all, because no code ran. If you see partial output and then a failure, it is not a syntax error in the file you ran.

# An unclosed bracket -> SyntaxError anchored at the OPENING bracket.
fields = ["id", "label",  # <-- Python points its caret HERE
          "length"        # the missing ] belongs after this item
print("loading")

This is the habit worth forming early: for an unclosed bracket, the reported line is where the broken construct began, so the repair usually belongs on a line further down, not on the line the caret points at. Python read to the end of the file still looking for the ] before it gave up and pointed back at the [.

DON'T:

  • DON'T hunt for a syntax error in your logic or data. It is purely structural, the fix is in how the code is typed, not in what it computes.
  • DON'T read the caret as "the character I typed wrong". For an unclosed bracket or quote it marks where the construct STARTED, and the missing partner belongs somewhere after it, often one or more lines below.

Runtime Error / Exception

What. A runtime error, also called an exception, is a failure that occurs while the program is running. The code parsed and started fine, but a specific operation could not complete, dividing by zero, converting non-numeric text, opening a missing file. Why. Runtime errors are the everyday reality of working with real data, because many of them depend on values, not structure. The same correct code runs fine on clean input and raises an exception on a malformed row. Understanding them is the foundation of writing resilient programs, since these are the failures you will later learn to catch and handle. When. A runtime error happens mid-execution, often after some output has already appeared. It is triggered by a particular value or state, a key that is not in the dictionary, an index past the end of a list, a string that will not convert to a number.

A runtime error is raised during execution, so earlier statements may have already run and produced output. This timing distinguishes it sharply from a syntax error in the file you run.

# This program PARSES fine and starts running...
print("starting")      # this line runs and prints
value = int("setosa")  # <-- ValueError raised HERE at runtime
print("done")          # never reached

The partial output is the signature. Something ran before the failure, which a syntax error in the file you ran can never produce.

Many runtime errors are tied to values, which is why they surface with real data. The exact same parsing code is correct for a clean row and raises for a malformed one.

def parse_length(field):
    return float(field)     # fine for "5.1", fails for "n/a"

print(parse_length("5.1"))  # 5.1, works
print(parse_length("n/a"))  # ValueError: could not convert string to float: 'n/a'

Nothing about parse_length is wrong, and that is the point. This is why a loader that works on a sample file crashes on the full dataset, one bad value in one row is enough.

Common runtime exceptions each correspond to a specific impossible operation, and recognizing them by sight speeds diagnosis enormously.

data = {"id": 1}
data["label"]  # KeyError: 'label' - key not in dict

xs = [10, 20]
xs[5]          # IndexError: list index out of range

10 / 0         # ZeroDivisionError: division by zero

"3" + 4        # TypeError: can only concatenate str (not "int") to str

Memorizing this handful pays for itself immediately, because a glance at the type usually tells you what went wrong before you have read another word.

DON'T:

  • DON'T treat a runtime error that a bad value raised as a code-structure problem. The code is valid, the failure is about a value, so the fix is usually validation or handling, not retyping the line.
  • DON'T assume code that runs on your test input is correct for all input. A runtime error often hides until a particular value appears in real data.

Logic Error

What. A logic error is code that runs to completion without raising anything but produces the wrong answer. There is no crash and no traceback, only an incorrect result. Why. Logic errors are the most dangerous kind, precisely because nothing announces them. A syntax error stops you, a runtime error stops you, but a logic error lets the program finish and report a confident, wrong number. In data work this can mean a model trained on miscomputed features, with no error message anywhere to warn you. When. Suspect a logic error whenever a program completes successfully but the output is not what it should be, an average that is too high, a count that is off by one, a filter that keeps the wrong rows. The absence of an exception is the trap.

A logic error produces no error message at all, and usually no message of any kind, though a library can print a warning that does not stop the run, as NumPy does for the square root of a negative number in Module 2. The program runs cleanly and returns a value that simply happens to be wrong, which is why it cannot be found by reading a traceback, there is none.

def average(values):
    total = 0
    for v in values:
        total += v
    return total / len(values) - 1  # <-- BUG: stray '- 1'

print(average([2, 4, 6]))           # prints 3.0, but the true average is 4.0

3.0 is a perfectly valid-looking number and the true average is 4.0. Only knowing the expected answer reveals the mistake, which is the whole problem with this category.

The classic logic errors are off-by-one mistakes, wrong operators, and inverted conditions, all of which run fine while computing the wrong thing.

# Off-by-one: misses the last element.
def total_through(xs, n):
    s = 0
    for i in range(n):  # <-- should be range(n + 1) to include index n
        s += xs[i]
    return s

# Inverted condition: keeps the wrong rows.
def high_scores(rows):
    # <-- '<' should be '>'
    return [r for r in rows if r["score"] < 0.5]

Both execute flawlessly and return the wrong thing, and neither has any marker in it that a reader could catch without already knowing the intent.

The defenses against logic errors are knowing the expected answer for a small case, and the defensive code and systematic debugging you will build in this section. They cannot be caught by the language itself.

DON'T:

  • DON'T assume "it ran without error" means "it is correct". A logic error runs perfectly and lies, verify the output against a known-correct small case.
  • DON'T try to find a logic error by reading a traceback. There is no traceback, you find it by checking results, not error messages.

Traceback

What. A traceback is the report Python prints when an exception goes unhandled. It shows the chain of function calls that led to the failure, ending with the exception type and message. It is read from the bottom up. Why. The traceback is the single most useful diagnostic Python gives you, and beginners waste enormous time because they read it in the wrong direction or skip to the middle. Read correctly, bottom line first, it tells you precisely what went wrong and where, turning a frightening wall of text into an exact address. When. You read a traceback every time an unhandled exception crashes a program. It is the first thing to consult, the starting point of every diagnosis, and the immediate input to the inspection-debugging discipline later in this section.

The discipline is read the bottom first. The last line names the exception type and message, the what. The lines above it, read upward, trace the call chain from where it failed back toward where it started, the where. The three blocks below write a data file, define a program that reads it, and show the crash that program produces, and the data file holds one line that is not a number, which is what makes it crash at all.

# 'data.txt', written first, so the program below has something to fail on.
with open("data.txt", "w", encoding="utf-8") as f:
    f.write("n/a\n")
# 'loader.py', with the line numbers the traceback below refers to.
def load(path):                  # line 2
    out = []                     # line 3
    # line 5
    for line in open(path, encoding="utf-8"):
        out.append(parse(line))  # line 6
    return out                   # line 7

def parse(line):                 # line 9
    return float(line.strip())   # line 10, fails on a non-numeric line

rows = load("data.txt")          # line 12, the top-level call
Traceback (most recent call last):
  File "/home/goku/dev/bootcamp/loader.py", line 12, in <module>
    rows = load("data.txt")          # line 12, the top-level call
           ^^^^^^^^^^^^^^^^
  File "/home/goku/dev/bootcamp/loader.py", line 6, in load
    out.append(parse(line))  # line 6
               ^^^^^^^^^^^
  File "/home/goku/dev/bootcamp/loader.py", line 10, in parse
    return float(line.strip())   # line 10, fails on a non-numeric line
           ^^^^^^^^^^^^^^^^^^^
ValueError: could not convert string to float: 'n/a'

Read the bottom line first: ValueError: could not convert string to float: 'n/a' tells you exactly what failed, a string 'n/a' could not become a float. Then read upward: the failure happened in parse (line 10), which was called from load (line 6), which was called from the top level (line 12). The bottom gives the cause, the upward path gives the location and how you got there.

A frame usually carries a line of ^ characters under its code, and Python leaves it out when every character of the line would get one, as under a bare main(). That is the caret line, and it marks the exact part of the statement that was executing, which matters when one line does several things. In the bottom frame the carets sit under float(line.strip()) rather than under the whole return, so the report is telling you the conversion failed, not the stripping. A related marker, ~, appears alongside the carets when the failing operation has an operand worth pointing at separately, for example an index or a key lookup, where the ~ underlines the thing being subscripted and the ^ the subscript itself.

The phrase "most recent call last" at the top is the key to the direction. The calls are listed oldest-first, so the deepest, most recent call, the one that actually failed, is at the bottom, right above the exception. That is why you read bottom-up.

Traceback (most recent call last):                                <- this header tells you the order
  File "/home/goku/dev/bootcamp/loader.py", line 12, in <module>  <- OLDEST call (the start)
  ...
  File "/home/goku/dev/bootcamp/loader.py", line 10, in parse     <- NEWEST call (where it broke)
ValueError: ...                                                   <- the exception, read FIRST

Reading top-down starts you at the entry point and walks you through callers before you reach the failure, which is why so much time gets lost in a traceback that was telling you the answer on its last line.

Inside the traceback, the File "...", line N, in FUNC lines give you the precise address of every frame: which file, which line number, which function. These are your jump targets, the failing line is the one in the frame just above the exception.

A traceback is printed to stderr, not to the stdout your print calls write to, the two separate streams of Lesson 0.2.2, and the difference matters the moment you redirect output. If you redirect a program's stdout to a file, the traceback still appears on your console, because it travels on stderr instead. When you capture a crash for a bug report or a log, capture stderr, that is the stream Python writes the diagnostic to.

DON'T:

  • DON'T read a traceback top to bottom looking for the cause. The cause is the bottom line, and the failing code is the frame directly above it, read upward from there.
  • DON'T ignore the middle frames when the bottom frame is in library code. The frame pointing at your file, nearest the bottom, is usually where you should look first.

Exception Type

What. An exception type is the named category of a runtime error, ValueError, KeyError, IndexError, TypeError, FileNotFoundError, and many more. The type is the first thing on the final line of a traceback. Why. The type is a precise vocabulary for failure. KeyError and IndexError both mean "I looked for something that was not there", but one is about a dictionary key and the other about a list position, and knowing which narrows the cause instantly. The type also drives exception handling, in the next lesson, you catch by type, so naming it correctly is essential. When. You read the exception type every time you diagnose a failure, and you name it every time you decide how to handle one. It is the single most informative token in a traceback, so learning the common types by heart pays off constantly.

The exception type appears on the last line, before the colon and message. Each common type maps to a specific kind of mistake, and the message after the colon adds the detail.

int("n/a")        # ValueError: invalid literal for int() with base 10: 'n/a'
{"a": 1}["b"]     # KeyError: 'b'
[1, 2][9]         # IndexError: list index out of range
"x" + 5           # TypeError: can only concatenate str (not "int") to str
open("gone.txt")  # FileNotFoundError: [Errno 2] No such file or directory: 'gone.txt'

The type names the category and the message after the colon names the detail, often the offending value, and you need both. KeyError: 'b' tells you which key, which is the part you actually go and fix.

The distinction between ValueError and TypeError trips up beginners and is worth nailing down: TypeError is "wrong kind of thing", ValueError is "right kind of thing, unacceptable content".

# TypeError: the TYPE is wrong for the operation.
len(5)      # TypeError: object of type 'int' has no len()

# ValueError: the type is fine, the VALUE is not.
int("3.5")  # ValueError: invalid literal for int() with base 10: '3.5'

The rule in one line: wrong type is TypeError, wrong value of an acceptable type is ValueError. No value of int would have made len work, whereas int("3") works perfectly.

Knowing the type also tells you the likely fix and which handler you will write later. A KeyError suggests a .get() with a default or a membership check, a ValueError from int/float suggests validating or skipping the field, a FileNotFoundError suggests checking the path.

DON'T:

  • DON'T confuse ValueError with TypeError. Wrong type of argument is TypeError, acceptable type with an unacceptable value is ValueError.
  • DON'T read only the type and skip the message. The type names the category, but the message after the colon gives the detail, often the specific offending value, which is what you actually fix.

Lab Exercises

Lab Exercise 1: Classify Four Failures, Then Cause Three on Purpose

For each snippet below, state whether it is a syntax error, a runtime error, or a logic error, and where it is a runtime error, name the exception type. Do not fix any of them, just classify, and for each say whether any output appears before the failure.

# (a)
def f(x)
    return x + 1
# (b)
nums = [1, 2, 3]
print(nums[3])
# (c)
def avg(xs): return sum(xs) * len(xs)  # intended: sum / len
# (d)
total = int("twelve")

Then write three tiny separate snippets that each deliberately produce a KeyError, an IndexError, and a ValueError from float(). Print a one-line note before each failing line saying which exception you expect, so the output shows how far the program got.

Lab Exercise 2: Read a Traceback, and Name What Each Type Means

Given the traceback below, state the exception type, the value that caused it, the function and line where it failed, and the original call that started the chain. Say which single frame you would open first and why.

Traceback (most recent call last):
  File "/home/goku/dev/bootcamp/run.py", line 12, in <module>
    main()
  File "/home/goku/dev/bootcamp/run.py", line 9, in main
    rows = load_csv("data.csv")
           ^^^^^^^^^^^^^^^^^^^^
  File "/home/goku/dev/bootcamp/run.py", line 5, in load_csv
    rows.append(int(line.split(",")[0]))
                ^^^^^^^^^^^^^^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'label'

Then, for each of the five lines below, write down the exception type you expect before running anything, then run them one at a time and record the message that follows the colon. Add a comment on the last two saying which one is about a wrong type and which about a wrong value, and how you can tell.

int("n/a")
{"a": 1}["b"]
[1, 2][9]
len(5)
int("3.5")

Solution 1: Classify Four Failures, Then Cause Three on Purpose

(a) Syntax error. The def header has no colon, so the file never parses and no output at all appears before the error report.

(b) Runtime error, IndexError. The list's indices run 0 to 2, or -1 to -3 from the end, so nums[3] is past the end. It parses and starts, so anything printed before it would appear.

(c) Logic error. It parses, it runs, it returns a number, and the number is wrong. sum(xs) * len(xs) should be sum(xs) / len(xs). Nothing is raised and no output marks it.

(d) Runtime error, ValueError. "twelve" is a string, which int will accept and attempt, but its content is not a numeral. Like (b), it parses and starts, so anything printed before it would appear.

# Each note prints BEFORE its failure, so the output shows how far we got.
print("expecting KeyError")
{"id": 1}["label"]

print("expecting IndexError")
[10, 20][5]

print("expecting ValueError")
float("n/a")

Run those three as separate files, or the first failure stops the rest. That itself is the lesson of this exercise, an unhandled exception ends the program, so only one of the three can ever run to completion in a single file.

DON'T:

  • DON'T classify by how bad the failure looks, classify by when it happens, at parse time, during the run, or never.
  • DON'T call (c) harmless because nothing raised, it is the only one of the four that could reach a report unnoticed.
Solution 2: Read a Traceback, and Name What Each Type Means

The exception type is ValueError. The offending value is the string 'label', which is almost certainly the file's header row being parsed as data. It failed in load_csv at line 5, in the expression int(line.split(",")[0]). The chain started at line 12 with the call to main(), which called load_csv at line 9.

Open the load_csv frame first, the one directly above the exception, because that is where the failing code lives. The frames above it tell you how you arrived, not what broke.

int("n/a")     # ValueError: invalid literal for int() with base 10: 'n/a'
{"a": 1}["b"]  # KeyError: 'b'
[1, 2][9]      # IndexError: list index out of range
len(5)         # TypeError: object of type 'int' has no len()
int("3.5")     # ValueError: invalid literal for int() with base 10: '3.5'
# 'len(5)' is the wrong TYPE, no integer has a length, so no value would fix it.
# 'int("3.5")' is the right type with a wrong VALUE, since int("3") works fine.

The root cause named in the traceback is worth stating plainly, because it is the single most common loader bug there is. The value 'label' is a column name, so the loop consumed the header row as if it were data, which is the failure the previous lesson's readline step exists to prevent.

DON'T:

  • DON'T start at the top frame, it is where you called from and it is almost never where you fix.
  • DON'T skip the message after the colon, the type tells you the category, and only the message can name the value.

Theory Assessment

Q1: When Each One Happens, and Which One Does Not Announce Itself

Name the three kinds of error and say, for each, at what moment it appears: before the program runs, during the run, or not at all. Then explain which of the three is the most dangerous in data work and why, given that the other two stop the program.

Hint: One of the three raises nothing and usually prints nothing. Ask what a program that finishes successfully with a wrong number costs you compared with one that crashes.

Q2: Which End to Read From, and Which Frame to Open

Explain why a traceback is read from the bottom up, and what the header "most recent call last" is telling you. Then, given a traceback with four frames, say which one names the exception, which one you should open first, and what the frames above that one are for.

Hint: The calls are listed oldest first. Work out which end the failure is therefore at, then ask what the remaining frames describe.

Q3: Naming the Failure Precisely

For each of float("1,5"), {"lr": 0.1}["LR"], ["train", "val"][2] and "loss: " + 0.5, name the exception type. Then state the rule that separates TypeError from ValueError, and use len(0.5) against int("") to illustrate it.

Hint: Two of the four are both "I looked for something that was not there" and they differ in what was being searched. For the rule, ask whether any value of that type would have worked.

Q4: Three Frames, One Wrong Guess

Read the following without running it. State every line of output, in order, including the exception type and the frame it is raised in.

def parse_row(line):
    fields = line.split(",")
    return {"id": int(fields[0]), "score": float(fields[1])}

def load(lines):
    return [parse_row(l) for l in lines]

print(load(["1,0.9", "2,0.8"]))
print(load(["1,0.9", "2"]))

Most readers predict the wrong exception type here. Name the type it actually raises, explain why the obvious guess is wrong, and say which of the three frames you would open to fix it and which line in that frame is genuinely at fault.

Hint: Work out what "2".split(",") produces before you decide what float() will be handed. The failing expression and the mistaken assumption are not in the same frame.


Answer 1:

Syntax errors in the file you run appear before the program runs. Python parses the whole file first, so an invalid structure means not one statement executes and nothing appears but the error report.

Runtime errors, also called exceptions, appear during the run. The file parsed and execution began, so output produced before the failing line has already appeared. Many depend on values rather than structure, which is why the same correct code succeeds on one row and raises on the next.

Logic errors never appear. The program parses, runs to completion, and returns a wrong answer, usually with no message of any kind.

The logic error is by far the most dangerous in data work, precisely because the other two stop you. A crash is a demand for attention and it costs you time. A logic error costs you the result: a feature computed with the wrong formula, a filter that kept the wrong rows, a metric reported to someone who acted on it. The output looks exactly like a correct output, so nothing prompts anyone to check.

DON'T treat "it ran without error" as a statement about correctness, it is only a statement about the first two categories.

Answer 2:

A traceback is read bottom up because the calls are printed oldest first, which the header states outright with "most recent call last". The most recent call is the one that actually failed, so it sits at the bottom, directly above the exception line.

Given four frames, the exception type and message are on the last line, below all the frames, and that is what you read first because it says what went wrong. The frame directly above the exception is the one you open first, because it holds the code that was executing when it broke. The frames above that are the callers, read upward, and they describe the route the program took to get there rather than the fault itself. They matter when the bottom frame is inside library code, in which case you look for the lowest frame that names a file of your own.

DON'T read from the top, that is where you started, not where you broke.

Answer 3:

float("1,5") raises ValueError, because a comma is not a decimal point to Python. {"lr": 0.1}["LR"] raises KeyError, because a key must match exactly, case included. ["train", "val"][2] raises IndexError. "loss: " + 0.5 raises TypeError.

KeyError and IndexError both mean "not found" and differ only in what was searched, a dictionary key against a list position.

The rule separating the other two: TypeError means the wrong kind of thing, ValueError means the right kind of thing with unacceptable content. len(0.5) is a TypeError because a float has no length and no float ever would, the operation does not apply to that type at all. int("") is a ValueError because a string is exactly what int expects to be handed, and int("3") works, so the type is fine and only this particular content fails.

DON'T read only the type, the message after the colon carries the detail, often the offending value, and that is what you go and fix.

Answer 4:

The first line prints the successfully parsed rows:

[{'id': 1, 'score': 0.9}, {'id': 2, 'score': 0.8}]

Then the second call raises, with three frames, and the exception is IndexError: list index out of range, raised in parse_row on the return line.

The obvious guess is ValueError, because the lesson has been full of float() failing on text and because the input "2" looks like a value problem. It is not. "2".split(",") returns ['2'], a list of one item, so fields[1] never reaches float at all, it fails first while trying to read a position that does not exist. float is not involved in the failure, and ValueError never occurs.

The traceback marks this precisely. Under the failing expression it prints ~~~~~~^^^, where the ~ sits under fields and the ^ under [1], so the operation that failed is the indexing of fields, not a call to float. That is the marker doing exactly the job described earlier in this lesson.

As for which frame to open, the exception is raised in parse_row and parse_row is not wrong. It assumes each line has two fields, which is a reasonable assumption for a parser to make. The genuine fault is in the data handed to load, the entry "2" that has no comma, so the fix belongs either at the call site that supplied it or in a validation step that rejects short rows before parsing. This is the case where the frame that raised and the frame that is wrong are different frames, and reading only the bottom one sends you to edit correct code.

DON'T assume the exception type from what the data looks like, work out what each operation is actually handed, and don't assume the raising frame is the guilty one.


Assignment

Assignment: A Loader That Diagnoses Instead of Crashing

Build the following in one file, in this order.

Write a loader that reads a CSV of id,score and, instead of stopping at the first bad row, records every failure and carries on. It returns two things: the list of successfully parsed records, and a list of error reports where each report carries the 1-based row number, the offending line, and the name of the exception type as a string.

Demonstrate it on a file you create in the same script, containing at least one row whose score is not a number, one row with the wrong number of fields, and at least two rows that are perfectly good. Print the good records, then print each error report on its own line.

Then answer, in a comment, a question the reports should now make easy: which two rows failed, and did they fail for the same reason? Print the set of distinct exception type names your run collected.

Finally, show the contrast the whole lesson rests on. Write a second loader with the same body but no error collection at all, call it on the same file inside a try that prints the exception and then stops, and add a comment stating how many rows the crashing version managed to report on compared with the diagnosing one.

Hints:

  • type(err).__name__ gives the class name as text, such as "ValueError", which is what you want in a report rather than the exception object itself.
  • enumerate(f, start=1) numbers the rows the way a person counts them, so an error report points at the line a text editor would show.

Solution:
# 'try'/'except' is formalized in the next lesson. Here it is only the tool that
# lets one bad row be recorded instead of ending the program.
def load_diagnosing(path):
    good = []
    errors = []
    with open(path, "r", encoding="utf-8") as f:
        # 1-based, as an editor counts
        for row_no, line in enumerate(f, start=1):
            line = line.strip()
            if line == "":
                continue
            try:
                # raises if the field count is wrong
                id_str, score_str = line.split(",")
                good.append({"id": int(id_str), "score": float(score_str)})
            except Exception as err:  # catch anything, record it, carry on
                errors.append({
                    "row": row_no,
                    "line": line,
                    # the CLASS NAME as text
                    "error": type(err).__name__,
                })
    return good, errors

with open("scores.csv", "w", encoding="utf-8") as f:
    f.write("1,0.9\n")
    f.write("2,n/a\n")
    f.write("3,0.7\n")
    f.write("4\n")
    f.write("5,0.5\n")

records, problems = load_diagnosing("scores.csv")
print(records)
for p in problems:
    print(p)
print(sorted({p["error"] for p in problems}))
# Rows 2 and 4 failed, and NOT for the same reason. Row 2 has two fields and a
# bad value, row 4 has one field and never reaches the conversion at all.
# The same parse with no collection: the first bad row ends everything.
def load_crashing(path):
    good = []
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line == "":
                continue
            id_str, score_str = line.split(",")
            good.append({"id": int(id_str), "score": float(score_str)})
    return good

try:
    load_crashing("scores.csv")
except Exception as err:
    print(type(err).__name__, err)
# The crashing version reported on ONE problem, the first. The diagnosing
# version reported on both, and still returned the three good rows.

The printed set is the point of the exercise, and what makes it interesting is that it holds one name rather than two. Both failures are ValueError, and they have nothing else in common.

Row 2 reaches the conversion and fails on content: float("n/a") is handed a string of the right type whose value is not a number. Row 4 never reaches any conversion at all. "4".split(",") returns the single-item list ['4'], and unpacking one item into the two names id_str, score_str fails immediately with ValueError: not enough values to unpack (expected 2, got 1). One row has a bad value, the other has the wrong shape, and the type name alone cannot tell them apart.

That is the argument for keeping the offending line in the report beside the type name. Grouping this run by exception type produces a single bucket of two rows and tells you nothing actionable, while reading the two line fields tells you instantly that one file has a data-quality problem and a structural problem, which are fixed in different places by different people.

It is also why the diagnosing loader catches Exception rather than a named type. At this stage you do not know the full set of ways a row can be malformed, and the report is how you find out. Narrowing the handler afterwards is the next lesson's work, and narrowing it before you have the evidence, to ValueError because that is the one you thought of, ends the whole load at the first row that fails some other way, and the report goes with it.

DON'T decide which exception to expect before you have run the loader over real data, and don't report the exception object where you meant its type name, print(err) gives the message without the category and the category is what you group by.


Additional Resources

Optional, and nothing later depends on either. Worth opening if you want a traceback in a log, or the full list of built-in exception names.

  • Official docs - traceback, the print_exc and format_exc entries. How to print a traceback where you choose, or capture it as text, which is what a log file or a bug report actually needs.
  • Official docs - Built-in Exceptions, the "Exception hierarchy", showing which exception types are subclasses of which.

Summary

This lesson is about diagnosis, and diagnosis begins with naming. Three kinds of failure, three different relationships to time. A syntax error in the file you run happens before the program runs, caught when Python reads the file, and it produces no program output and no traceback, only the error report, a refusal to start. A runtime error happens while the program runs, often triggered by a particular value rather than by how the code is written, which is why the same correct line can succeed on one row and crash on the next. And a logic error happens, in a sense, never, the program runs to completion and hands back a confident, wrong answer with no complaint at all. Learning to feel which of these you are facing, often from the symptoms alone, is half the battle, because each demands a completely different response.

The traceback is the instrument that turns a runtime crash from frightening to precise, and the single most important habit is reading it in the right direction. The bottom line is the exception, the what. The frame directly above it is where the code broke, the where. Each frame above that is a caller, retracing the path back to the start. Beginners read top-down and drown in the entry point and the library frames before they ever reach the real failure, and they waste hours doing it. Read bottom-up, name the exception type, note the detail the message hands you, and a wall of text becomes a street address. The exception type itself is a vocabulary worth memorizing, because KeyError, IndexError, ValueError, and TypeError each point at a different mistake, and in the next lesson the type is also what you will catch by name.

That is the bridge out of this lesson. You can now read what went wrong, but reading is passive, the program still crashed. The next step is to do something about it: to wrap the risky conversion so a single bad row is skipped instead of fatal, to catch the specific exception type you anticipated while letting exceptions of every other type still surface, and to raise errors of your own when your code detects something it cannot accept. Diagnosis becomes response, and a script that dies on the first surprise becomes software that survives a messy file.


Cheat Sheet

Part A - Reference Table

Error kind When detected Output How to find
Syntax, in the file you run parse time (before run) the error report only reported line, or further down
Runtime / Exception during execution any output so far, then traceback read traceback bottom-up
Logic never (runs fine) wrong result verify vs known answer
Exception Means Typical cause
ValueError right type, bad value int("n/a")
TypeError wrong type for op "a" + 1
KeyError key absent (dict, set) d["missing"]
IndexError index out of range (list, str, tuple) xs[99]
FileNotFoundError path does not exist open("gone")

Part B - Compact Code Block

# Reading a traceback (bottom-up):
#   LAST line     -> exception type + message  (the WHAT)
#   frame above   -> the line that failed      (the WHERE)
#   frames above  -> callers, oldest at top    (the PATH)
# "most recent call last": deepest call at bottom

int("n/a")                     # ValueError  (value bad, type ok)
"a" + 1                        # TypeError   (incompatible types)
{"a": 1}["b"]                  # KeyError    (no such key)
[1, 2][9]                      # IndexError  (no such index)

# read the error's type name while handling:
try:
    float("x")
except ValueError as err:
    print(type(err).__name__)  # ValueError
←Previous: Section 5, Lesson 3
Next: Lesson 2→

Questions about this lesson: #module-1 on Discord

Found an error in this lesson? Report it

Your progress
Current streak
🔥 0
Lessons marked today
0 / 3
Longest streak
0

Sign in

The bootcamp

  • Curriculum
  • How To
  • Pricing
  • Changelog
  • Future

The community

  • Discord
  • Reviews
  • Contact

Follow

  • Instagram
  • X
  • TikTok
  • Reddit

Legal

  • Terms
  • Privacy
  • Refunds

Coming soon on November 16, 2026

Theme