Exceptions in Python — theory with the faculty admin office: try, except, else, finally, raise and assert (and the comma grade that knocked the script over)
The first day I helped out with a script for the faculty admin office (turn the grades from a form into numbers and work out the average), I ran it full of confidence and it stopped at the third grade. Someone had typed 7,5 with a decimal comma, the Spanish way, and Python shows no mercy:
# The admin office script: turn the grades from the form into numbers
grades_txt = ['8', '6.5', '7,5', '9'] # someone typed a decimal comma, Spanish style
grades = []
for text in grades_txt:
grades.append(float(text)) # with '7,5' Python cannot go on
print('Grade stored:', grades[-1])
print('Average:', sum(grades) / len(grades))
Output:
Grade stored: 8.0
Grade stored: 6.5
Traceback (most recent call last):
File "hook.py", line 5, in <module>
grades.append(float(text)) # with '7,5' Python cannot go on
~~~~~^^^^^^
ValueError: could not convert string to float: '7,5'
Two grades stored, a Traceback and no average at all. The rest of the list was never processed. That red message is an exception: Python is telling you it has found something it cannot resolve and, since nobody told it what to do, it stops the program. In FP1 you learned to avoid errors by checking first with if. In FP2 you take the next step: catching errors when they happen, deciding what to do with them and raising them yourself when you spot something wrong.
Today we cover the whole topic with the faculty admin office as the thread: credits that arrive as text, records that don’t exist, deadlines that pass. If you come from the last FP1 topic, Text files in Python, you have already met FileNotFoundError: now you will know what to do with it. When you finish, the exceptions practice is waiting with four complete programs.
Table of Contents
What is an exception?
An exception is an object Python creates when an operation cannot be completed: turning 'NP' (a no-show) into a number, dividing by zero, asking for position 5 of a 3-item list. As soon as it is created, the program stops running line by line and the exception starts to climb through the functions that were running, looking for someone to catch it.
Look at this program: report calls average, average calls to_number and to_number does the float() that fails.
def to_number(text):
return float(text) # the exception is born here
def average(list_txt):
numbers = [to_number(t) for t in list_txt]
return sum(numbers) / len(numbers)
def report(student, list_txt):
print('Calculating the average for', student)
print('Average:', average(list_txt))
report('Laura', ['7', '8.5', 'NP']) # NP = no-show: not a number
Output:
Calculating the average for Laura
Traceback (most recent call last):
File "stack.py", line 15, in <module>
report('Laura', ['7', '8.5', 'NP']) # NP = no-show: not a number
~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "stack.py", line 12, in report
print('Average:', average(list_txt))
~~~~~~~^^^^^^^^^^
File "stack.py", line 6, in average
numbers = [to_number(t) for t in list_txt]
~~~~~~~~~^^^
File "stack.py", line 2, in to_number
return float(text) # the exception is born here
ValueError: could not convert string to float: 'NP'
The Traceback looks like a wall of text, but it is easy to read once you know the trick: start with the last line. ValueError: could not convert string to float: 'NP' tells you what happened. The line above tells you where: to_number, line 2. Everything above that is the path that led there, from the outermost call (<module>, the main program) to the innermost. The ~~~~ and ^^^^ marks point at the exact piece of each line that was running.
Look at the drawing: the call goes down from function to function and the exception travels the other way. Since none of the four has a try, it climbs all the way to the top and the program stops. “Calculating the average for Laura” was printed (it happened before the error), but “Average:” never was.
A Traceback is not your enemy: it is the best error report you will ever get. Before searching online, read the last line and the one above. Most of the time you already know what to fix.
Types of exceptions in Python
Each problem has its own exception class. These are the ones you will see most in FP2. To show them all without the program stopping at the first one, each line goes inside its own try (explained in the next section; for now look only at the name and the message):
# Seven lines, seven different exceptions. Each goes in its own try so the program carries on.
grades = {'Laura': 8.5}
cases = ["int('NP')", "10 / 0", "['FP1', 'FP2'][2]", "grades['Pablo']",
"'Mark: ' + 7", "open('marks_2027.txt')", "grades.upper()"]
for code in cases:
try:
eval(code) # runs the text as if it were code
except Exception as err: # Exception: the base of them all (more later)
print(f'{code:<23} {type(err).__name__}: {err}')
Output:
int('NP') ValueError: invalid literal for int() with base 10: 'NP'
10 / 0 ZeroDivisionError: division by zero
['FP1', 'FP2'][2] IndexError: list index out of range
grades['Pablo'] KeyError: 'Pablo'
'Mark: ' + 7 TypeError: can only concatenate str (not "int") to str
open('marks_2027.txt') FileNotFoundError: [Errno 2] No such file or directory: 'marks_2027.txt'
grades.upper() AttributeError: 'dict' object has no attribute 'upper'
Each one tells a different story:
| Exception | When it is raised | Admin office example |
|---|---|---|
ValueError | The type is fine, the value is not | int('NP'): it is a str, but not a number |
ZeroDivisionError | You divide (or use % or //) by zero | Price per credit with 0 credits |
IndexError | A position that does not exist in a list or string | The third subject in a list of two |
KeyError | A key that does not exist in a dictionary | Pablo’s record, which is not in grades |
TypeError | An operation between incompatible types | 'Mark: ' + 7 (text plus number) |
FileNotFoundError | The file does not exist | The 2027 records, not created yet |
AttributeError | The object has no such method or attribute | A dictionary has no upper() |
Mixing up ValueError and TypeError. int('NP') is a ValueError because you can pass text to int() (int('7') works); what fails is that particular text. 'Mark: ' + 7 is a TypeError because there is no way to add a str and an int, whatever the number.
Exceptions come in families
Exceptions are not loose: they form a tree, and each one “is a child” of a more general one. You can see anyone’s chain with __mro__:
# Whose 'child' is each exception? __mro__ lists the chain up to the base
for exc_class in [ZeroDivisionError, KeyError, IndexError, FileNotFoundError, ValueError, KeyboardInterrupt]:
chain = [c.__name__ for c in exc_class.__mro__ if c is not object]
print(' -> '.join(chain))
# In practice: an except for the parent catches all its children
try:
['FP1', 'FP2'][2]
except LookupError as err: # LookupError = IndexError + KeyError
print('Caught as LookupError:', type(err).__name__)
Output:
ZeroDivisionError -> ArithmeticError -> Exception -> BaseException KeyError -> LookupError -> Exception -> BaseException IndexError -> LookupError -> Exception -> BaseException FileNotFoundError -> OSError -> Exception -> BaseException ValueError -> Exception -> BaseException KeyboardInterrupt -> BaseException Caught as LookupError: IndexError
This has a very practical consequence: an except for the parent catches all its children. except LookupError catches both IndexError and KeyError, and except Exception catches almost everything. Almost: KeyboardInterrupt (what Ctrl+C raises) hangs straight from BaseException, so that an except Exception does not stop you from killing a program that has frozen. The green boxes are custom exceptions, which we create at the end of the article.
try/except vs if/else — when to use each
The question that comes up most in FP2: if I can check with an if, why do I want a try? There are two styles and both are valid:
| Style | Idea | When it fits |
|---|---|---|
Check first (if) | “Look before you leap” | The check is easy and cheap: if credits != 0: before dividing |
Try and catch (try) | “Easier to ask forgiveness than permission” (EAFP) | You don’t know if it will fail until you try: converting what someone typed, opening a file, looking something up in a dictionary |
Checking with an if whether '7,5' is a valid number is a mess (a sign? a dot? spaces? '1e3'?). But float('7,5') already knows how: you try it and, if it fails, you catch it. With files it gets worse: even if you check that a file exists, someone can delete it between your if and your open(). That is why Python code uses the EAFP style so much.
Ask yourself “who knows better whether this is valid, me with an if or the operation itself?”. If it is the operation (int(), open(), d[key]), use try.
The try/except structure
try:
# code that may raise an exception
except SomeException:
# what to do if THAT type happens
The rule that takes longest to sink in: as soon as one line in the try raises an exception, the rest of the try is skipped and Python jumps straight to the matching except. Afterwards, the program carries on below the whole block. You can see it with the admin office working out the price per credit of a 735 € enrolment:
def price_per_credit(amount, credits_txt):
print('Received:', repr(credits_txt))
try:
credits = int(credits_txt) # (1) may raise ValueError
print(' (1) converted:', credits)
price = amount / credits # (2) may raise ZeroDivisionError
print(' (2) price per credit:', price)
except ValueError:
print(' (3) credits must be a whole number')
except ZeroDivisionError:
print(' (4) zero credits: nothing to enrol in')
print(' moving on to the next enrolment')
for item in ['30', 'thirty', '0']:
price_per_credit(735.0, item)
Output:
Received: '30' (1) converted: 30 (2) price per credit: 24.5 moving on to the next enrolment Received: 'thirty' (3) credits must be a whole number moving on to the next enrolment Received: '0' (1) converted: 0 (4) zero credits: nothing to enrol in moving on to the next enrolment
Go through the three calls calmly:
- With
'30', everything works: (1) and (2) are printed, and 735 / 30 = 24.5 € per credit. - With
'thirty',int()fails on the first line of thetry. Neither (1) nor (2) is printed: the jump to (3) is immediate. - With
'0',int('0')works, so (1) is printed; the division fails and jumps to (4).
In all three cases “moving on to the next enrolment” is printed: the program did not break. That is the difference from the hook at the start.
Specific except — catching concrete exceptions
In the example above there is one except for each problem we expect, with its own message. That is the recommended way: if you know what failed, you can tell the user something useful (“credits must be a whole number”) instead of a “something went wrong” that helps nobody.
Catching several exceptions in one block
If several errors are handled the same way, group them in a tuple:
for item in ['thirty', '0']:
try:
price = 735.0 / int(item)
except (ValueError, ZeroDivisionError) as err: # two types, the same handling
print(f'{item!r}: invalid value ({type(err).__name__})')
Output:
'thirty': invalid value (ValueError) '0': invalid value (ZeroDivisionError)
The parentheses are compulsory. except ValueError, ZeroDivisionError: (no parentheses) has been a syntax error since Python 3, even if you see it in old code online.
Generic except — the difference that confuses most
Here is the first big trap of the topic: the order of the except clauses matters. Python tries them top to bottom and enters the first one that matches. If you put the parent before the child, the child never runs:
def read_credits(text):
try:
return 735.0 / int(text)
except Exception: # the parent of almost all of them goes FIRST...
print('Something went wrong with', repr(text))
except ValueError: # ...so this is never reached
print('Not a number:', repr(text))
read_credits('thirty')
Output:
Something went wrong with 'thirty'
'thirty' causes a ValueError, but a ValueError also is an Exception (it is its child), so the first except catches it and the second is dead. Python does not warn you: the program works, just with the wrong message. The rule: from the most specific to the most general.
The second trap is even worse: a bare except: (or except Exception:) added “just in case”. Look what it does with a simple typo:
def average(grades):
try:
return sum(grades) / len(grade) # typo: grade instead of grades
except: # 'just in case'... and it hides the typo
return 0.0
print('Laura's average:', average([8, 7, 9]))
Output:
Laura's average: 0.0
An average of 0.0 with an 8, a 7 and a 9? The typo len(grade) raises a NameError, the except: catches it without asking and returns 0.0 as if nothing had happened. If you catch only what you expect (an empty list, ZeroDivisionError), the typo comes to light at once:
def average(grades):
try:
return sum(grades) / len(grade) # the same typo
except ZeroDivisionError: # only what we expect: empty list
return 0.0
print('Laura's average:', average([8, 7, 9]))
Output:
Traceback (most recent call last):
File "hidden_fixed.py", line 8, in <module>
print('Laura's average:', average([8, 7, 9]))
~~~~~~~^^^^^^^^^^^
File "hidden_fixed.py", line 3, in average
return sum(grades) / len(grade) # the same typo
^^^^^
NameError: name 'grade' is not defined. Did you mean: 'grades'?
It even suggests the fix: Did you mean: 'grades'?. An error Python shows you on the first run is worth far more than one that hides and gives you false averages for weeks.
Using except: or except Exception: so that “the red error doesn’t show up”. You don’t fix the problem: you hide it. On top of that, a bare except: also catches Ctrl+C and can stop you from ending the program. Use it only at the end of a chain of specific except clauses, and always show what happened.
Accessing exception information
With as you name the exception object and can read what it carries:
try:
int('7,5')
except ValueError as err: # err is the exception object
print('Message :', err) # str(err): the text
print('Type :', type(err).__name__)
print('args :', err.args) # tuple with what was passed when it was created
Output:
Message : invalid literal for int() with base 10: '7,5'
Type : ValueError
args : ("invalid literal for int() with base 10: '7,5'",)
print(err) gives the message (the same as the last line of the Traceback), type(err).__name__ the name of the type and err.args the tuple with what was passed when it was created. The first one is the most common: print('Error:', err).
The additional blocks — else and finally
A try can have two more blocks:
else: runs only if thetryfinished without an exception. It is the place for whatever depends on everything having gone well, but does not need protecting.finally: runs always, with or without an exception, caught or not. It is the place for tidying up: closing a file, releasing something, closing the counter.
def charge(amount, credits_txt):
print(f'--- enrolment with {credits_txt!r} credits')
try:
price = amount / int(credits_txt)
except ValueError:
print('except: not a number')
except ZeroDivisionError:
print('except: zero credits')
else:
print(f'else: {price:.2f} € per credit') # only if try finished without an exception
finally:
print('finally: closing the counter') # always, whatever happens
for c in ['30', 'NP', '0']:
charge(735.0, c)
Output:
--- enrolment with '30' credits else: 24.50 € per credit finally: closing the counter --- enrolment with 'NP' credits except: not a number finally: closing the counter --- enrolment with '0' credits except: zero credits finally: closing the counter
The table sums up the three paths. Why not put the price print at the end of the try and forget about else? Because everything inside the try is “protected”: if that line raised a ValueError for some other reason, you would mistake it for the credits one. else leaves inside the try only what can really fail.
And now the most surprising thing about finally: it runs even if there is a return inside the try and even if nobody catches the exception:
def serve():
try:
print('try: serving Laura')
return 'Laura served'
finally:
print('finally: screen off (before returning)')
print(serve())
def find_record():
try:
print('try: looking for Pablo's record')
records = {'Laura': 8.5}
return records['Pablo'] # KeyError that nobody catches
finally:
print('finally: runs even though nobody catches the KeyError')
find_record()
Output:
try: serving Laura
finally: screen off (before returning)
Laura served
try: looking for Pablo's record
finally: runs even though nobody catches the KeyError
Traceback (most recent call last):
File "finally_return.py", line 21, in <module>
find_record()
~~~~~~~~~~~^^
File "finally_return.py", line 16, in find_record
return records['Pablo'] # KeyError that nobody catches
~~~~~~~^^^^^^^^^
KeyError: 'Pablo'
In serve(), the return already has 'Laura served' ready, but before leaving the function Python runs the finally: that is why “screen off” comes out before the result. In find_record() there is no except at all: the finally runs anyway and then the KeyError keeps climbing and stops the program.
Thinking finally “fixes” the error. It does not catch it: it just runs and lets the exception carry on. To handle the error you need an except.
raise — throwing your own exceptions
What does raise do in Python? It throws an exception yourself. So far it was Python raising them when something could not be done. But some situations are perfectly valid for Python and not for your program: an 11 is a lovely number, but it is not a grade (grades go from 0 to 10 in Spain). With raise you turn that rule of yours into an exception like any other:
def validate_grade(grade):
if not 0 <= grade <= 10:
raise ValueError(f'grade fuera de rango: {grade}') # we raise it ourselves
return grade
for n in [7.5, 11, -1]:
try:
print('Stored:', validate_grade(n))
except ValueError as err:
print('Rejected:', err)
Output:
Stored: 7.5 Rejected: grade fuera de rango: 11 Rejected: grade fuera de rango: -1
You write raise followed by the exception created with its message: raise ValueError('…'). Whoever calls validate_grade decides what to do (here, print “Rejected”). And if nobody catches it, the same happens as with Python’s own:
def validate_grade(grade):
if not 0 <= grade <= 10:
raise ValueError(f'grade fuera de rango: {grade}')
return grade
validate_grade(12) # nobody catches it: the program stops
Output:
Traceback (most recent call last):
File "raise_alone.py", line 7, in <module>
validate_grade(12) # nobody catches it: the program stops
~~~~~~~~~~~~~~^^^^
File "raise_alone.py", line 3, in validate_grade
raise ValueError(f'grade fuera de rango: {grade}')
ValueError: grade fuera de rango: 12
That is the beauty of raise: the function that detects the problem does not need to know how to solve it. validate_grade does not know whether you are in a form (and should ask again) or in a script processing a thousand records (and should log it). It just warns; the caller decides.
Pick the type that best describes the problem. A value outside what is allowed: ValueError. The wrong type (text where you expected a number): TypeError. And write messages that say which value failed: f'grade out of range: {grade}' saves a lot of debugging time.
raise without arguments — propagating exceptions
Inside an except, a bare raise, with nothing after it, raises the same exception again. It is useful for doing something (warning, counting attempts) and still letting the problem climb:
# What the person would type (on your computer, replace read_line() with input()):
TYPED = iter(['a', 'b', 'c'])
def read_line(prompt):
text = next(TYPED) # the next 'typed' value
print(prompt + text) # we show it as it would look in the terminal
return text
def read_credits():
attempts = 0
while True:
try:
return int(read_line('Credits: '))
except ValueError:
attempts += 1
print(' Enter a whole number')
if attempts == 3:
print(' Third failure: passing the problem up')
raise # re-raises the SAME exception
try:
read_credits()
except ValueError as err:
print('Enrolment cancelled:', err)
Output:
Credits: a Enter a whole number Credits: b Enter a whole number Credits: c Enter a whole number Third failure: passing the problem up Enrolment cancelled: invalid literal for int() with base 10: 'c'
The first two failures are handled inside the function: a warning and another try. On the third, raise re-raises the ValueError from int('c') exactly as it was, with its original message, and the outer try catches it and cancels the enrolment. Outside an except there is no “active” exception to re-raise: a bare raise gives RuntimeError (you will see it in the exercises).
The programs in this topic don’t use input(): what the person would type goes in a list (TYPED) and read_line() prints it as it would appear in the terminal. That way every output you see is real. On your computer, replace read_line(...) with input(...) and it will work the same.
Custom exceptions
When your errors have names of their own (“past the deadline”, “credits not allowed”), the clearest thing is to create your own exception classes. All you need is a class that inherits from Exception (inheritance is the topic after next; for now just copy the class Name(Exception): form):
class EnrolmentError(Exception):
"""Base of all enrolment errors."""
class CreditsError(EnrolmentError):
"""Credits outside what the rules allow (in this example, 24 to 78)."""
class DeadlineError(EnrolmentError):
"""The request arrives after the deadline."""
def enrol(name, credits, day):
if day > 15:
raise DeadlineError(f'{name}: the deadline was day 15 (it arrived on day {day})')
if not 24 <= credits <= 78:
raise CreditsError(f'{name}: {credits} credits (se permiten de 24 a 78)')
return f'{name}: enrolled in {credits} credits'
requests = [('Laura', 60, 3), ('Pablo', 90, 7), ('Ana', 60, 20)]
for s in requests:
try:
print(enrol(*s))
except CreditsError as err: # the specific child, first
print('Credits ->', err)
except EnrolmentError as err: # any other enrolment error
print(type(err).__name__, '->', err)
Output:
Laura: enrolled in 60 credits Credits -> Pablo: 90 credits (se permiten de 24 a 78) DeadlineError -> Ana: the deadline was day 15 (it arrived on day 20)
Three important things:
- The family:
CreditsErrorandDeadlineErrorhang fromEnrolmentError(green boxes in the hierarchy diagram). That is why the secondexcept EnrolmentErrorcatches Ana’sDeadlineErrorwithout naming it. - The order: the child’s
except(CreditsError) goes before the parent’s, as we saw above. - The docstring is enough as the class body; no
passneeded. By convention, names end inError, like Python’s.
The credit rules (24 to 78) and the deadline (day 15) are made up for the example: they are not the ULPGC ones.
Sometimes you want to translate a Python error into one of yours without losing the original. That is what raise … from err is for:
class CreditsError(Exception):
pass
def read_credits(text):
try:
return int(text)
except ValueError as err:
raise CreditsError(f'"{text}" are not credits') from err # translates the error
read_credits('sixty')
Output:
Traceback (most recent call last):
File "from_err.py", line 7, in read_credits
return int(text)
ValueError: invalid literal for int() with base 10: 'sixty'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "from_err.py", line 12, in <module>
read_credits('sixty')
~~~~~~~~~~~~^^^^^^^^^
File "from_err.py", line 9, in read_credits
raise CreditsError(f'"{text}" are not credits') from err # translates the error
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
CreditsError: "sixty" are not credits
The Traceback shows both: the original ValueError and, after “The above exception was the direct cause of the following exception”, your CreditsError. Anyone reading it knows which of your rules was broken and what caused it underneath.
Assertions — assert
An assertion is a check that, if the program is written correctly, should always hold. If it doesn’t, it raises AssertionError with the message you give:
def average(grades):
assert len(grades) > 0, 'average() needs at least one grade'
return sum(grades) / len(grades)
print(average([8, 6.5, 9]))
print(average([])) # the assertion fails
Output:
7.833333333333333
Traceback (most recent call last):
File "assertion.py", line 7, in <module>
print(average([])) # the assertion fails
~~~~~~~^^^^
File "assertion.py", line 2, in average
assert len(grades) > 0, 'average() needs at least one grade'
^^^^^^^^^^^^^^^
AssertionError: average() needs at least one grade
The form is assert condition, 'message' (the message is optional). It is roughly the same as if not condition: raise AssertionError('message'). Check: (8 + 6.5 + 9) / 3 = 23.5 / 3 = 7.8333…, the first line of the output.
But one detail changes everything: assertions can be switched off. If you run Python with the -O (optimise) option, they disappear:
import subprocess, sys
# The same code, run with plain python3 and with python3 -O (optimised)
code = """def average(n):
assert len(n) > 0, 'no grades'
return sum(n) / len(n)
print(average([]))"""
for options in ([], ['-O']):
r = subprocess.run([sys.executable, *options, '-c', code], capture_output=True, text=True)
print('python3', *options, '->', r.stderr.strip().splitlines()[-1])
Output:
python3 -> AssertionError: no grades python3 -O -> ZeroDivisionError: division by zero
With -O the assertion does not exist and the program reaches the division: ZeroDivisionError. That is why an assert must never be the only thing protecting your program from data that comes from outside.
When to use assert vs raise
| Use… | For… | Example |
|---|---|---|
raise | Things that can happen even if your code is right: data from a person, files, the network | An 11 typed into the form |
assert | Things that should never happen if your code is right: checking your own assumptions while programming | average() called with an empty list because of a bug of yours |
Validating user input with assert. It works in your tests, but a single -O makes the validation vanish. For user input, if + raise.
Common mistakes with exceptions
A huge try around half the program. If 30 lines can raise ValueError for different reasons, the except doesn’t know which one it was. Put in the try only the lines that can fail.
except ValueError, TypeError: without parentheses. Since Python 3 it is a syntax error: except (ValueError, TypeError):.
raise ValueError when you meant to say something. It works without a message, but the Traceback will only say ValueError. Always write what failed and with which value.
Using finally to return a value. A return inside finally swallows any exception that was climbing. In finally, only tidy up.
Watch it step by step in Python Tutor
Python Tutor runs your code line by line and draws the variables and function calls. Choose Python 3, paste this program and step through with Next:
def credit_price(amount, credits):
if credits == 0:
raise ZeroDivisionError('zero credits')
return amount / credits
def charge(credits):
try:
price = credit_price(735.0, credits)
except ZeroDivisionError as err:
print('Not charged:', err)
else:
print('Price per credit:', price)
finally:
print('Counter free')
charge(30)
charge(0)
Output:
Price per credit: 24.5 Counter free Not charged: zero credits Counter free
What to watch:
- In the first call,
charge(30)creates its frame and inside itcredit_priceappears withcredits = 30: two stacked frames, as in the call stack diagram. - On the way back,
priceis 24.5 and the arrow jumps toelse; then tofinally. - In
charge(0), watch theraisestep: thecredit_priceframe disappears without returning anything and the arrow lands in theexceptofcharge. - The variable
pricenever gets created in the second call: the assignment never finished. That is why the priceprintgoes inelse. - In both calls
print('Counter free')runs:finallynever misses.
Quick summary
| I want to… | I write… | Detail |
|---|---|---|
| Protect code that may fail | try: … except Type: | The rest of the try after the error is skipped |
| Handle two types the same way | except (ValueError, ZeroDivisionError): | With parentheses |
| Read the message | except ValueError as err: print(err) | type(err).__name__, err.args |
| Run code only if all went well | else: | After the except clauses |
| Run code always | finally: | Even with return or uncaught |
| Raise an error of mine | raise ValueError('message') | With a message that says the value |
| Re-raise the one I am handling | raise (inside an except) | Outside an except: RuntimeError |
| Translate an error | raise MyError('…') from err | Both appear in the Traceback |
| My own type | class MyError(Exception): | With families: class Child(MyError): |
| Check my assumptions | assert condition, 'message' | Switched off by -O: never for validating data |
Three ideas to take away:
- A Traceback is read bottom up: the last line says what happened and the one above, where.
- Catch the specific and in order, from child to parent. A generic “just in case”
excepthides real errors. - The function that detects the problem raises (
raise); the one that knows what to do catches (except).finallyalways tidies up.
Now, let’s put it into practice: in the Python exceptions practice we build an enrolment form that rejects bad data, an unbreakable calculator, a grade reader and a fractions class with its own exceptions. If you study Java too, the same topic is in exceptions in Java (there it is try/catch and throw). And once you master it, the next FP2 topic is Python classes, where custom exceptions will make full sense.
Did this article help you? Here is how you can support SergioLearns →




What is an operating system — theory with your laptop on a study afternoon, a write() call and a CPU that never rests (and the three programs that weren’t running at the same time)