Python Module 7 – Errors, exceptions and context managers
Handling exceptions with try and except
Catch the exceptions your code can handle with try and except in Python, run code only on success with else, clean up with finally, and choose what to catch.
What you will learn
- Catch specific exceptions and handle them meaningfully
- Read the exception hierarchy to decide what to catch
- Use else and finally clauses correctly
- Choose between checking first and handling the exception
Before you start
What you’ll learn · Before you start
What you will learn
- Catch specific exceptions and handle them meaningfully
- Read the exception hierarchy to decide what to catch
- Use else and finally clauses correctly
- Choose between checking first and handling the exception
Before you start
On this page
int("three") cannot make a number and prices["cofee"] cannot find a key, so each of these lines raises an
exception: An object Python raises when a line cannot do its job, such as the ValueError of int("three"). A try statement can handle it; otherwise it ends the program with a traceback., Python’s way of reporting that a line could not do its job. Unless something
catches it, the exception ends the program with a traceback: The report Python prints when an exception ends a program: the calls that led to the failing line, oldest first, and on the last line the exception's type and message.. A try statement is how you
catch one. Put the code that may fail under try:, and under except with the name of an exception type, the code
that should run instead when that kind of exception happens. Python then runs your
exception handler: An except clause and its block: the code that runs instead when the try block raises an exception of the class the clause names, or of a subclass of that class. and carries on after the whole statement. Any other exception passes
through untouched, as if the try were not there.
Below you can step through real runs and watch where Python jumps when a line fails. You will decide how narrow a
handler should be, follow else and finally along every path a try statement can take, and weigh checking first
against trying first.
An exception skips the rest of the try block
This function turns the text a user typed into a quantity. It prints what it is doing at each step, so you can see which lines run:
def parse_quantity(text):
print(f"parse_quantity({text!r})")
try:
quantity = int(text)
print(" converted")
except ValueError:
print(" not a whole number, so 1")
quantity = 1
print(" after the try statement")
return quantity
print(parse_quantity("3"))
print(parse_quantity("three"))
print(parse_quantity(None)) # int(None) raises TypeError, which no clause names Output (exit status 1)
parse_quantity('3')
converted
after the try statement
3
parse_quantity('three')
not a whole number, so 1
after the try statement
1
parse_quantity(None)
Printed as an error (standard error)
Traceback (most recent call last):
File "first_try.py", line 15, in <module>
print(parse_quantity(None)) # int(None) raises TypeError, which no clause names
~~~~~~~~~~~~~~^^^^^^
File "first_try.py", line 4, in parse_quantity
quantity = int(text)
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 first_try.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Follow the three calls:
"3"converts without trouble. The wholetryblock runs, theexceptclause is skipped, and the function carries on after the statement."three"makesint()raiseValueErroron line 4. Python abandons the rest of thetryblock at that line, soconvertedis never printed. It finds the clauseexcept ValueError:, runs it, and then carries on after the statement as if nothing had gone wrong.Nonemakesint()raiseTypeError. No clause namesTypeError, so the exception leaves the function at once.after the try statementis not printed, and the traceback ends the program.
The same three texts go through one try statement in the loop below. Step through it and watch the numbered chip,
which marks the line that runs next: when int() fails on line 5, the chip jumps over line 6 to the except clause.
Program · three_texts.py
texts = ["3", "three", None]
counts = []
for text in texts:
try:
counts.append(int(text))
print("converted", text)
except ValueError:
print("skipped", text)
print("after the statement")
print(counts) Variables
- just taken by the loop
- changed in this step
- passed by the loop
- read next
Printed so far
Nothing printed yetconverted 3 after the statement skipped three after the statement Error TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.
The drawing shows the variables when the program ends, and the program's 18 steps are listed below.
Change the program and run it here. Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Every step as text
- Start. No line has run yet, so there are no variables. Python starts with line 1.
- Line 1.
texts = ["3", "three", None]makestextsa list of 3 items. - Line 2.
counts = []makescountsa list of 0 items. - Line 3.
for text in texts:takes the item at position 0 oftexts:textis'3'. - Line 4.
try:runs.try:checks nothing by itself. It starts the block whose exceptions theexceptclause below may handle. - Line 5.
counts.append(int(text))adds3to the end ofcounts. - Line 6.
print("converted", text)printsconverted 3. - Line 9.
print("after the statement")printsafter the statement. - Line 3.
for text in texts:takes the item at position 1 oftexts:textis'three'. - Line 4.
try:runs. - Line 5.
counts.append(int(text))leavescountsas it was. Key pointint("three")raisesValueErrorbeforeappend()is called, socountsis unchanged. Line 6 is skipped: the next line is theexceptclause. - Line 7. Line 7 runs. The
ValueErrormatchesexcept ValueError:, so this block runs. Afterwards the loop carries on after the whole statement, at line 9. - Line 8.
print("skipped", text)printsskipped three. - Line 9.
print("after the statement")printsafter the statement. - Line 3.
for text in texts:takes the item at position 2 oftexts:textisNone. - Line 4.
try:runs. - Line 5.
counts.append(int(text))leavescountsas it was.int(None)raisesTypeError, a different kind of exception. - Line 7. Line 7 runs. Key point A
TypeErroris not aValueError, so the clause does not match, and no other clause is left. The exception leaves the statement and ends the program: lines 9 and 10 never run.
Three rules decide what happens. Python compares the exception with the except clauses from top to bottom and runs
the first one that matches, so at most one handler runs. A clause matches the exception’s own class and every
class it is derived from, which the next section uses. And except ValueError as err: gives the exception object a
name, err, for the handler to use: its message is str(err) and its type type(err). Python deletes that name when
the clause ends, so copy the object to another variable if you need it afterwards.
Asking again until the answer fits
A program that reads input usually needs more than one handler. This one asks for a number of seats, and its input contains two mistakes before a good answer, then ends before the second question is answered:
def ask_int(prompt, low, high):
"""Ask until the answer is a whole number from low to high; None when the input runs out."""
while True:
try:
text = input(prompt)
except EOFError:
print("\nNo more input, giving up.")
return None
try:
number = int(text)
except ValueError:
print(f"{text!r} is not a whole number.")
continue
if low <= number <= high:
return number
print(f"{number} is not from {low} to {high}.")
seats = ask_int("Seats (1-6): ", 1, 6)
print("Seats:", seats)
children = ask_int("Children (0-4): ", 0, 4)
print("Children:", children) Input (standard input)
two 9 3
Output
Seats (1-6): 'two' is not a whole number. Seats (1-6): 9 is not from 1 to 6. Seats (1-6): Seats: 3 Children (0-4): No more input, giving up. Children: None
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ask_number.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Each try statement guards one call and names the one exception that call raises. input() raises EOFError when
there is nothing left to read, and the function then gives up and returns None. int() raises ValueError for text
that is not a whole number, and continue starts the loop again. The range check sits outside both statements,
because comparing two numbers raises nothing: a number that is too big is an ordinary if, not an exception. The
prompts and answers run together because the answers came from the input shown above the output rather than from a
keyboard.
Key idea
When a line of the try block raises, the rest of the block is skipped and the first clause that matches the
exception runs; an exception that no clause matches travels on as if the try statement were not there.
Checkpoint
Check: How a try statement handles an exception
A clause catches its class and every subclass
Exception classes form a family tree, and a clause that names a class also catches every subclass: A class derived from another one, its base class, and counted as a kind of it. KeyError is a subclass of LookupError, so a clause that names LookupError also handles a KeyError. of it. The part of the tree you meet most is drawn below. Each box holds the classes derived from it, so a clause that names a box handles everything inside it:
Part of the exception hierarchy: each box holds the classes derived from it
Text description of the diagram
The diagram is a set of nested boxes. Each box is an exception class and holds the classes derived from it, so a clause that names a box handles every class inside it.
- The outer box is BaseException, the root of every exception.
- Inside it, one box names SystemExit, raised by sys.exit(), KeyboardInterrupt, raised when the user presses Ctrl-C, and GeneratorExit. They are not errors, so they stand outside Exception.
- The other box inside BaseException is Exception, the base of everything you normally catch.
- Inside Exception: ArithmeticError, which holds ZeroDivisionError and OverflowError; LookupError, which holds IndexError and KeyError; OSError, which holds FileNotFoundError, PermissionError and TimeoutError; ValueError, which holds UnicodeDecodeError; TypeError; and a box for NameError, AttributeError and many more.
BaseException is the root of every exception. SystemExit, raised by sys.exit(), KeyboardInterrupt, raised when
the user presses Ctrl-C, and GeneratorExit sit directly under it: they are not errors, so they stand outside
Exception, the base of everything you normally catch. So except LookupError: handles a bad list index and a
missing dictionary key alike, and except OSError: handles every error that the operating system reports. When you
handle a subclass and its base class differently, put the subclass first: Python takes the first clause that matches,
so an except FileNotFoundError: written after except OSError: would never run. The full tree is on the Built-in
Exceptions page of the Python documentation.
Comparing messages instead of types
if str(err) == "...": breaks when a message is reworded, and messages do change between versions: Python 3.14
rewrote the one for a list used as a dictionary key. Name the type in the clause instead, as in except KeyError:,
and let the class decide.
Several exceptions in one clause
When two exceptions need the same handling, list them in one clause. Since Python 3.14 the brackets around the list
are optional, as long as the clause has no as:
def to_number(value):
try:
return float(value)
except ValueError, TypeError: # Python 3.14: no brackets needed without "as"
return None
print(to_number("2.5"), to_number("two"), to_number(None))
def describe(value):
try:
return f"{float(value):.2f}"
except (ValueError, TypeError) as err: # with "as", the brackets stay
return f"{type(err).__name__}: {err}"
print(describe("2.5"))
print(describe("two"))
print(describe([2.5])) Output
2.5 None None 2.50 ValueError: could not convert string to float: 'two' TypeError: float() argument must be a string or a real number, not 'list'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bracketless.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
float("two") raises ValueError, while float(None) and float([2.5]) raise TypeError, and each clause handles
both. With as, the brackets are still required, because without them it would be unclear what the name after as
stands for:
try:
price = float("12,50")
except ValueError, TypeError as err:
print("Not a price:", err) Output (exit status 1)
Printed as an error (standard error)
File "bracketless_as.py", line 3
except ValueError, TypeError as err:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: multiple exception types must be parenthesized when using 'as'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bracketless_as.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Version note
New in Python 3.14 (PEP 758): except ValueError, TypeError: without brackets. Python 3.13 and older reject it
with SyntaxError: multiple exception types must be parenthesized, so code that must run on older versions keeps
the brackets: except (ValueError, TypeError): works everywhere.
Checkpoint
Check: Which exceptions a clause matches
A broad handler hides the bugs you meant to see
A handler should name the exceptions that it knows how to deal with, and nothing else. A wider net catches mistakes you meant to see. This function was meant to return 0.0 for an empty list, but a typo in it raises a different exception:
def average_broad(marks):
try:
return sum(marks) / len(mark) # a typo: mark instead of marks
except Exception:
return 0.0 # meant for an empty list, but it hides the typo as well
def average_narrow(marks):
try:
return sum(marks) / len(mark) # the same typo
except ZeroDivisionError:
return 0.0 # only an empty list ends up here
print("broad:", average_broad([70, 80, 90]))
print("narrow:", average_narrow([70, 80, 90])) Output (exit status 1)
broad: 0.0
Printed as an error (standard error)
Traceback (most recent call last):
File "too_broad.py", line 16, in <module>
print("narrow:", average_narrow([70, 80, 90]))
~~~~~~~~~~~~~~^^^^^^^^^^^^^^
File "too_broad.py", line 10, in average_narrow
return sum(marks) / len(mark) # the same typo
^^^^
NameError: name 'mark' is not defined. Did you mean: 'marks'?
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 too_broad.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
except Exception: caught the NameError of the misspelt mark, so average_broad() returned a believable but
wrong 0.0 instead of 80.0, and nothing pointed at the bug. average_narrow() names the only exception an empty list
can cause, ZeroDivisionError, so the typo reaches the traceback, where Python even suggests the fix.
Handling an exception where nothing can be done about it
A helper that catches an exception, prints a message and returns a made-up value leaves its caller working with
bad data, as average_broad() did. Let the exception travel up to the code that can retry, ask again or report
it.
A bare except catches too much
except: with no type at all matches every exception, SystemExit and KeyboardInterrupt included. Here it stops
sys.exit() from ending a program:
import sys
def save_and_quit():
print("Saving...")
sys.exit(0) # ends the program by raising SystemExit
try:
save_and_quit()
except: # a bare except catches SystemExit as well
print("Something went wrong, carrying on.")
print("Still running: sys.exit() did not end the program.")
try:
save_and_quit()
except Exception: # SystemExit is not an Exception, so this clause lets it through
print("Not printed.")
print("Not printed either: the program has ended.") Output
Saving... Something went wrong, carrying on. Still running: sys.exit() did not end the program. Saving...
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bare_except.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The first try statement swallowed the SystemExit, so the program printed that it was still running. The second
one names Exception, which SystemExit is not derived from, so the exception passed through and ended the program
without a traceback, as an unhandled SystemExit always does. A loop whose body sits in a try statement with a bare
except: behaves the same way with Ctrl-C: the KeyboardInterrupt lands in the handler and the loop keeps going.
Name the exceptions you expect, or at most Exception, which leaves both of these alone.
except: pass, or except Exception: pass
The error disappears, and so does every clue to it. If a failure really may be ignored, name its type, as in
except FileNotFoundError: pass, and write a comment that says why.
Checkpoint
Check: Catching only what you can handle
else holds the code that runs only on success
An else clause after the except clauses runs when the try block ran to its end without an exception; a return,
break or continue that leaves the try block early skips it. It may look like a longer way to write the same
thing, but it keeps the try block down to the lines whose errors you mean to handle. These two functions look up an
item in a shop’s stock and then its shelf:
stock = {"pen": 12, "notebook": 4}
shelf = {"pen": "A3"} # nobody entered the notebook's shelf
def where_is(item):
try:
count = stock[item]
place = shelf[item] # a second lookup, inside the try block
except KeyError:
return f"We do not sell {item!r}."
return f"{count} on shelf {place}"
def where_is_else(item):
try:
count = stock[item]
except KeyError:
return f"We do not sell {item!r}."
else:
return f"{count} on shelf {shelf[item]}" # the except clause above does not catch this
print(where_is("pen"))
print(where_is("eraser"))
print(where_is("notebook")) # a wrong answer: notebooks are in stock
print(where_is_else("notebook")) # the real problem, reported as a KeyError Output (exit status 1)
12 on shelf A3 We do not sell 'eraser'. We do not sell 'notebook'.
Printed as an error (standard error)
Traceback (most recent call last):
File "else_clause.py", line 26, in <module>
print(where_is_else("notebook")) # the real problem, reported as a KeyError
~~~~~~~~~~~~~^^^^^^^^^^^^
File "else_clause.py", line 20, in where_is_else
return f"{count} on shelf {shelf[item]}" # the except clause above does not catch this
~~~~~^^^^^^
KeyError: 'notebook'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 else_clause.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
where_is() puts both lookups in the try block, so the KeyError of the missing shelf entry is handled as if the
shop did not sell notebooks: a confident, wrong answer. where_is_else() guards only the stock lookup. The shelf
lookup sits in else, where a KeyError is not handled by the clause above it, so the real problem, a notebook
without a shelf entry, shows up as a traceback that names it.
A try block that guards half the function
The more lines a try block holds, the more likely a handler meant for one line catches an exception from
another, as where_is() did. Keep the line that may fail in the try block, and move the rest into else or
after the statement.
Checkpoint
Check: Code that runs only on success, in else
finally runs on every way out
A finally clause runs whatever happens in the try block: when it ends normally, when it returns, and when it
raises an exception that nobody here handles. That makes it the place to give back what the block borrowed, such as
a connection, a lock or a temporary file:
class Connection:
def __init__(self, name):
self.name = name
print(f"open {name}")
def send(self, data):
if not data:
raise ValueError("nothing to send")
print(f"sent {data!r} to {self.name}")
def close(self):
print(f"closed {self.name}")
def upload(data):
conn = Connection("server")
try:
conn.send(data)
return "done"
finally:
conn.close() # runs before the function returns, and also when send() raises
print(upload("marks.csv"))
try:
upload("")
except ValueError as err:
print("upload failed:", err) Output
open server sent 'marks.csv' to server closed server done open server closed server upload failed: nothing to send
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 finally_cleanup.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
In the first upload, close() runs after return "done" has worked out the value and before the function hands it
back. In the second, send() raises ValueError; the finally clause closes the connection, and then the exception
continues to the caller, which handles it. For files and similar resources, the with statement, a later lesson of
this module, does this for you, but it relies on the same rule.
One thing does not belong in a finally clause: return, break or continue. Leaving the clause that way throws
away an exception that was on its way out:
def risky():
try:
raise ValueError("the real problem")
finally:
return "all good" # this discards the ValueError
print(risky()) Output
all good
Printed as an error (standard error)
finally_return.py:5: SyntaxWarning: 'return' in a 'finally' block return "all good" # this discards the ValueError
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 finally_return.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Version note
New in Python 3.14 (PEP 765): the compiler warns with SyntaxWarning when return, break or continue would
leave a finally block, as above. The behaviour has not changed (the function still returns "all good" and the
ValueError is lost); Python 3.13 and older do the same without a warning.
Checkpoint
Check: Clean-up in finally
The four clauses run in a fixed order on three paths
A try statement needs at least one except clause or a finally clause. The clauses always come in the same order:
try, then the except clauses, then else (only when there is an except clause), then finally. Which of them
run depends on what the try block does:
What runs, and in which order, in a try statement with except, else and finally
Text description of the diagram
The diagram has three columns, one for each way a try statement can end. Each column reads from top to bottom.
- No exception: the try block runs to its end, then the else clause runs, then the finally clause, and then the program carries on with the code after the try statement.
- An exception that an except clause names: the try block stops at the line that raised it, the except clause that names its type (or one of its base classes) runs, then the finally clause, and then the code after the try statement.
- An exception that no except clause names: the try block stops at the line that raised it, the finally clause runs, and the exception carries on to the code that called this one, as if there were no try statement. The else clause never runs when the try block raised an exception.
The same three texts as before now go through a statement with all four clauses. Step through it and hold each path
against the diagram: "3" takes the first column, "three" the second and None the third.
Program · four_clauses.py
texts = ["3", "three", None]
counts = []
for text in texts:
try:
counts.append(int(text))
except ValueError:
print("skipped", text)
else:
print("added", text)
finally:
print("finally runs")
print("after the statement")
print(counts) Variables
- just taken by the loop
- changed in this step
- passed by the loop
- read next
Printed so far
Nothing printed yetadded 3 finally runs after the statement skipped three finally runs after the statement finally runs Error TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.
The drawing shows the variables when the program ends, and the program's 21 steps are listed below.
Change the program and run it here. Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Every step as text
- Start. No line has run yet, so there are no variables. Python starts with line 1.
- Line 1.
texts = ["3", "three", None]makestextsa list of 3 items. - Line 2.
counts = []makescountsa list of 0 items. - Line 3.
for text in texts:takes the item at position 0 oftexts:textis'3'. - Line 4.
try:runs. - Line 5.
counts.append(int(text))adds3to the end ofcounts. No exception, so theexceptclause is skipped. Theelseblock runs next, at line 9. - Line 9.
print("added", text)printsadded 3. - Line 11.
print("finally runs")printsfinally runs.finallyruns after theelseblock, on the way out of the statement. - Line 12.
print("after the statement")printsafter the statement. - Line 3.
for text in texts:takes the item at position 1 oftexts:textis'three'. - Line 4.
try:runs. - Line 5.
counts.append(int(text))leavescountsas it was. - Line 6. Line 6 runs. The
ValueErrormatches, so this block runs and theelseblock is skipped. - Line 7.
print("skipped", text)printsskipped three. - Line 11.
print("finally runs")printsfinally runs.finallyruns after theexceptblock too. - Line 12.
print("after the statement")printsafter the statement. - Line 3.
for text in texts:takes the item at position 2 oftexts:textisNone. - Line 4.
try:runs. - Line 5.
counts.append(int(text))leavescountsas it was. - Line 6. Line 6 runs. A
TypeErrordoes not matchexcept ValueError:. Before the exception leaves the statement,finallyruns. - Line 11.
print("finally runs")stops the program withTypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'. Key pointfinallyran although nothing handled theTypeError. Once it ends, the exception carries on and stops the program, so lines 12 and 13 never run.
finally runs is printed three times, once for each text, but after the statement only twice. For None, the
TypeError left the statement as soon as finally had run, and the program ended before lines 12 and 13.
Key idea
else runs only when the try block raised nothing, an except clause only for the exceptions it names, and
finally on every way out, before an exception that nothing handled goes on.
Checkpoint
Check: The four clauses on three paths
Look before you leap, or ask forgiveness?
There are two ways to deal with an operation that may fail. Look before you leap (LBYL: Look before you leap: checking with an if that an operation can work before doing it. Other code can change the data between the check and the action, so the check can be out of date.)
checks first, with an if. Easier to ask for forgiveness than permission (EAFP: Easier to ask for forgiveness than permission: trying an operation and handling the exception if it fails, instead of checking first. The Python documentation calls it the common Python style.) just
tries, and handles the exception. Python code tends towards EAFP, and not only out of habit: between a check and the
action, something else can change the data. This example books a seat on a bus both ways, with a function that stands
in for another booking made at the worst moment:
free_seats = {"12A": "window", "12B": "aisle"}
def book_lbyl(seat, meanwhile):
if seat in free_seats: # look before you leap
meanwhile() # stands in for another booking that runs at this very moment
kind = free_seats.pop(seat) # KeyError when the seat has gone in between
return f"booked {seat} ({kind})"
return f"{seat} is taken"
def book_eafp(seat, meanwhile):
meanwhile()
try:
kind = free_seats.pop(seat) # easier to ask forgiveness than permission
except KeyError:
return f"{seat} is taken"
return f"booked {seat} ({kind})"
def nothing():
pass
def someone_takes_12b():
free_seats.pop("12B", None)
print(book_eafp("12A", nothing))
print(book_eafp("12B", someone_takes_12b))
free_seats["12B"] = "aisle" # the seat is free again
print(book_lbyl("12B", someone_takes_12b)) Output (exit status 1)
booked 12A (window) 12B is taken
Printed as an error (standard error)
Traceback (most recent call last):
File "check_then_act.py", line 32, in <module>
print(book_lbyl("12B", someone_takes_12b))
~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
File "check_then_act.py", line 7, in book_lbyl
kind = free_seats.pop(seat) # KeyError when the seat has gone in between
KeyError: '12B'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_then_act.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
book_lbyl() checked that 12B was free, but by the time it called pop() another booking had taken the seat, and the
check did not protect it from the KeyError. book_eafp() has no gap to fall into: taking the seat and finding out
that it has gone are one step. Step through both bookings and watch the other booking slip into the gap:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Look before you leap. The first call of the program has booked 12A, so free_seats holds 12B alone. book_lbyl() checks first: is "12B" in free_seats? It is, so the check passes and the function goes on to take the seat.
- Someone else is quicker. Between the check and the booking, another booking takes 12B. In the program, the function passed as meanwhile stands in for it and removes 12B from free_seats.
- Acting on an old answer. book_lbyl() now calls free_seats.pop("12B"), trusting a check that is no longer true. The seat has gone, so pop() raises KeyError, which nothing in the function handles.
- The same race, once more. book_eafp() meets the same race, with 12B free and the other booking about to take it, and it checks nothing beforehand. (In the program this booking runs first; line 31 then makes 12B free again for book_lbyl().)
- Someone else is quicker. The other booking removes 12B, exactly as before.
- Ask forgiveness. book_eafp() calls pop("12B") inside a try statement. The KeyError comes back, and its except KeyError clause turns it into the answer "12B is taken".
- No gap to fall into. Taking the seat and finding out that it has gone are one step, so no other booking can slip in between. The check of book_lbyl() was out of date the moment it was made.
With threads, several programs or files on disk, that gap is real, and a file can disappear between
os.path.exists() and open() in the same way.
LBYL is still the clearer choice when nothing else can change the value in between, and the test is cheap and says
what you mean: if divisor == 0: reads better than catching ZeroDivisionError from a long expression. It is also
the safer choice when a failure halfway would leave work half done, such as checking that every field of a form is
filled in before you save any of them.
Checkpoint
Check: Checking first or trying first
Practise: divide safely and collect bad values
The exercise asks for two functions that each handle exactly the exception they expect and let every other one
through. safe_divide() turns a division by zero into None, and parse_ints() collects a message for each text
that is not a whole number, while the TypeError of a bug and the KeyboardInterrupt of Ctrl-C still get out. Its
sample tests run in your browser:
Exercise · Easy · Python
Divide safely and collect bad values
Write two functions in safe_parse.py. Each one handles exactly the exception it expects and lets every other exception through.
safe_divide(a, b)returnsa / b, orNonewhenbis 0. A text such as"7"is a mistake in the code that called it, sosafe_divide("7", 2)must still raiseTypeError.parse_ints(strings)converts each text of a list to anintand returns a pair(values, errors):valuesis the list of the numbers it could convert, in order, anderrorshas one message for each text that is not a whole number, written asf"{text!r} is not a whole number".parse_ints(["10", "ten", "7"])returns([10, 7], ["'ten' is not a whole number"]). Spaces around a number are fine, asint()allows them.
A value that is not text at all, such as None, is a bug in the caller: parse_ints(["1", None]) raises TypeError instead of collecting it. And when the user presses Ctrl-C while the list is converted, KeyboardInterrupt must stop the function, not end up in errors. One sample test fakes that moment with a text whose conversion raises KeyboardInterrupt.
Starter code · safe_parse.py
def safe_divide(a, b):
"""Return a / b, or None when b is 0."""
# Replace this line with your code.
return a / b
def parse_ints(strings):
"""Return (values, errors): the whole numbers of strings, and a message for each text that is not one."""
# Replace this line with your code.
return [], [] The sample tests · test_safe_parse.py
from safe_parse import parse_ints, safe_divide
class Interrupted(str):
"""A text whose conversion is interrupted, as when the user presses Ctrl-C at that moment."""
def __int__(self):
raise KeyboardInterrupt
def __float__(self):
raise KeyboardInterrupt
def strip(self, chars=None):
raise KeyboardInterrupt
def test_divide():
"""divides as / does"""
assert safe_divide(7, 2) == 3.5
assert safe_divide(-9, 3) == -3.0
def test_divide_by_zero():
"""returns None when b is 0"""
assert safe_divide(1, 0) is None
assert safe_divide(0, 0) is None
def test_divide_lets_type_error_through():
"""raises TypeError for a text: that is a bug in the caller, not a zero"""
try:
safe_divide("7", 2)
except TypeError:
return
assert False, "safe_divide('7', 2) should raise TypeError"
def test_parse_good_values():
"""converts every whole number, with spaces around it allowed"""
assert parse_ints(["4", " 15 ", "-2"]) == ([4, 15, -2], [])
assert parse_ints([]) == ([], [])
def test_parse_collects_bad_values():
"""keeps going and describes each text that is not a whole number"""
values, errors = parse_ints(["10", "ten", "", "3.5", "7"])
assert values == [10, 7]
assert errors == ["'ten' is not a whole number", "'' is not a whole number", "'3.5' is not a whole number"]
def test_parse_lets_type_error_through():
"""raises TypeError for a value that is not text"""
try:
parse_ints(["1", None])
except TypeError:
return
assert False, "parse_ints(['1', None]) should raise TypeError"
def test_ctrl_c_is_not_swallowed():
"""lets KeyboardInterrupt through instead of treating it as a bad value"""
try:
parse_ints(["1", Interrupted("2"), "3"])
except KeyboardInterrupt:
return
assert False, "parse_ints caught KeyboardInterrupt" A hint
Put only the line that can fail inside try, and name the one exception you expect: ZeroDivisionError for the division, ValueError for int(text). In the loop of parse_ints, append the number in the try block (or in an else clause) and the message in the except clause, so that the loop carries on with the next text. A bare except: or except Exception: would also catch the TypeError and the KeyboardInterrupt that the tests want to see.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
The questions below cover handlers, matching clauses, else, finally and the two styles of dealing with failure.
You get one at a time, starting where your answers are weakest, and each choice says why it is right or what it
misses:
Practice that adapts to you
Practise this lesson
20 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · In a try statement with except, else and finally clauses, which clauses run when the try block
succeeds, and which run when it raises an exception that one except clause handles?
When the try block runs to its end without an exception, the except clauses are skipped, else runs, then
finally, and the program continues after the statement. When a line of the try block raises an exception that a
clause names, the rest of the try block is skipped, the first matching except clause runs, else is skipped, and
finally runs before the program continues after the statement. Only one except clause runs, even if several would
match. If the exception matches no clause, finally still runs, and then the exception carries on to the caller. A
return in the try block also passes through finally on its way out.
Key takeaways
- A
trystatement runs its block; when a line raises an exception, the rest of the block is skipped and the firstexceptclause whose type matches runs. Unmatched exceptions continue to the caller. - A clause matches the class it names and every subclass, so
except LookupError:handlesIndexErrorandKeyError; put subclasses before their base classes. Python 3.14 acceptsexcept ValueError, TypeError:without brackets, and withasthe brackets stay. - Name the exceptions you can handle.
except Exception:hides bugs such as typos, and a bareexcept:also stopssys.exit()and Ctrl-C from working. elseholds the code that should run only when thetryblock succeeded, so its errors are not handled by mistake;finallyruns on every path and suits clean-up, but neverreturnfrom it.- Prefer trying and handling the exception (EAFP) when the data can change between a check and its use; check first (LBYL) when the test is cheap and nothing can change in between.
Terms in this lesson
- EAFP
- Easier to ask for forgiveness than permission: trying an operation and handling the exception if it fails, instead of checking first. The Python documentation calls it the common Python style.
- exception
- An object Python raises when a line cannot do its job, such as the ValueError of int("three"). A try statement can handle it; otherwise it ends the program with a traceback.
- exception handler
- An except clause and its block: the code that runs instead when the try block raises an exception of the class the clause names, or of a subclass of that class.
- LBYL
- Look before you leap: checking with an if that an operation can work before doing it. Other code can change the data between the check and the action, so the check can be out of date.
- subclass
- A class derived from another one, its base class, and counted as a kind of it. KeyError is a subclass of LookupError, so a clause that names LookupError also handles a KeyError.
- traceback
- The report Python prints when an exception ends a program: the calls that led to the failing line, oldest first, and on the last line the exception's type and message.
References
- The Python Tutorial: handling exceptions (Python Software Foundation)
- The Python Tutorial: defining clean-up actions (Python Software Foundation)
- The Python Language Reference: the try statement (Python Software Foundation)
- Built-in Exceptions: exception hierarchy (Python Software Foundation)
- Built-in Exceptions: KeyboardInterrupt and SystemExit (Python Software Foundation)
- sys.exit() (Python Software Foundation)
- What's New In Python 3.14: except and except* without brackets (PEP 758) (Python Software Foundation)
- What's New In Python 3.14: control flow in finally blocks (PEP 765) (Python Software Foundation)
- PEP 758: Allow except and except* expressions without parentheses (Python Software Foundation)
- PEP 765: Disallow return/break/continue that exit a finally block (Python Software Foundation)
- Glossary: EAFP (Python Software Foundation)
- Glossary: LBYL (Python Software Foundation)
- What's New In Python 3.14: improved error messages (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress