Course outline · 0% complete

0/32 lessons0%

Course overview →

try / except

lesson 9-2 · ~11 min · 28/32

Handling errors instead of crashing

Some errors are not bugs, they are expected: users type nonsense, files go missing. A try/except block lets you attempt risky code and take a recovery path when a specific error occurs:

try:
    n = int(raw)
    print("ok:", n)
except ValueError:
    print("bad:", raw)

How it flows: Python runs the try block. If nothing goes wrong, except is skipped entirely. If the named error is raised anywhere in the try, Python abandons the rest of the block and jumps into the except.

Two rules of good taste:

  1. Name the error (except ValueError:). A bare except: also swallows real bugs like typos, hiding them from you.
  2. Keep the try small: only the line(s) that can legitimately fail.
try block n = int(raw) print("ok:", n) no error rest of try runs, then except SKIPPED ValueError raised inside int() except ValueError recovery path, program lives The remaining try lines are abandoned, so print("ok:", n) never runs on the error path. A KeyError would match no clause here and would still crash the program.
The two paths through a try and except block. When nothing goes wrong the whole try body runs and the except clause is skipped entirely, shown along the top. When int raises a ValueError the remaining try lines are abandoned and control jumps into the handler, so the ok message never prints and the program survives on the recovery path. An error of a type the clause does not name, such as a KeyError, matches nothing and still crashes.

Good and bad input through the same code

One conversion handles all three values, and the loop keeps running through the failure rather than stopping at it.

for raw in ["42", "abc", "7"]:
    try:
        n = int(raw)
        print("ok:", n)
    except ValueError:
        print("bad:", raw)

Output

ok: 42
bad: abc
ok: 7

The first and third values convert cleanly, so the except clause is skipped entirely on those passes. The middle value raises a ValueError inside int(raw), which means the print("ok:", n) on the next line never runs, and control jumps to the handler instead.

The important part is the last line of output. Because the try sits inside the loop, recovering from one bad value leaves the loop intact and the remaining items are still processed. Placing the try outside the loop would have abandoned everything after the first failure.

When a try body guarded by except ValueError: raises a KeyError, the program still crashes with that KeyError.

An except clause catches only the types it names. A KeyError is unrelated to ValueError, so it passes straight through the handler as though it were not there and propagates upward.

That behavior is deliberate rather than a limitation. Handling means anticipating a specific failure and having a sensible response to it, and a handler that caught everything would also swallow the typos and logic bugs you need to see. Writing except: with no type is exactly the mistake that turns a five-minute fix into an afternoon of confusion, because the program keeps running while quietly hiding its real problem.

Writing safe_div

A return can sit inside a try, which makes it easy to give a function one result on success and another on a specific failure.

def safe_div(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return 0

print(safe_div(10, 2))
print(safe_div(5, 0))

Output

5.0
0

The first call divides normally and returns 5.0, a float because / always produces one. The second call raises ZeroDivisionError before any value can be returned, so the handler runs and returns 0 instead.

Note the mismatch in those two results, since it is a real design question rather than a typo. One path returns a float and the other an int, which is fine here but would matter to a caller doing strict comparisons. Returning 0.0 would keep the type consistent, and returning None would let the caller distinguish a genuine zero result from a failed division.

The same pattern applied to real input is what keeps an interactive program from dying on a typo.

raw = input()
try:
    n = int(raw)
    print("double:", n * 2)
except ValueError:
    print("please type a number")

Input

hello

Output

please type a number

The conversion fails on hello, so n is never assigned and the print on the following line never runs. Control moves to the handler, which reports the problem in language a user can act on rather than showing a traceback.

Notice that input() sits outside the try. Reading a line cannot raise a ValueError, so including it would only widen the block for no benefit, and keeping the try down to the lines that can genuinely fail is what makes the handler's meaning obvious.