Python exceptions practice — four programs that don’t break: an enrolment form, a calculator, a grade reader and fractions (and the “12,5” that refused to be a number)
When I built my first “bulletproof” enrolment form, I gave it to a classmate to break. It took her ten seconds: for the entry grade she typed 12,5, with a decimal comma, and the program answered with a bare could not convert string to float, no explanation. It didn’t crash (I had wrapped it in a try), but it didn’t say anything useful either. That is today’s lesson: a robust program is not only one that doesn’t stop; it is one that knows what failed and says so.
In the Python exceptions theory you saw try, except, else, finally, raise and custom exceptions. Today we use them in four complete programs, all of medium level: an enrolment form that keeps asking until the data is valid, a calculator that survives anything you type, a grade reader that reads a file with broken lines and a fractions class with its own family of exceptions. The data looks like real data, with gaps, commas, text where numbers should be and files that don’t exist, because that is exactly where exceptions prove their worth.
Table of Contents
Before you start
Make sure you are clear on:
- That when a line in the
tryfails, the rest of thetryis skipped and Python enters the first matchingexcept. - The difference between
else(only if there was no exception) andfinally(always). - That
raise ValueError('message')throws an exception of yours and the caller’sexcept ValueErrorcatches it. - How to create a custom exception:
class MyError(Exception):.
If any of these feels shaky, review the theory before going on.
None of the programs uses input(): what the person would type goes in a list (TYPED) and the read_line() function prints it as it would appear in the terminal, so you see real output with good and bad data. On your computer, delete TYPED and read_line() and write input(...) wherever it says read_line(...).
Python exceptions practice — Program 1: Robust data validator
The admin office wants an enrolment form: name, age (16 to 99), entry grade (5 to 14, the Spanish university entry scale) and mode (in-person or online). What makes it tricky is that each value can fail in two ways: not being the right type ('twelve' is not an integer) or being the right type but out of range (12 years old). The program must ask again until the value is valid, without ever breaking.
The idea
One function per data type (ask_int, ask_float, ask_option), all with the same pattern: a while True with a try inside. If the value is valid, return (which leaves the loop). If not, the except warns and the loop goes round again. The trick is the range: instead of an if with its own print, we raise a ValueError, so that both failures (int()‘s and the range one) end up in the same except.
Before programming, think about how many ways each value can be wrong. For the age, two (not a number, out of range). For the mode, only one (it is none of the options), so there is no conversion that can fail there.
The code
# What the person would type (on your computer, replace read_line() with input()):
TYPED = iter(['Laura Pérez', 'twelve', '12', '22', '15', '12,5', '12.5', 'virtual', 'In-person'])
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 ask_int(prompt, minimum, maximum):
while True: # keeps asking until the value is valid
try:
value = int(read_line(prompt)) # ValueError if it is not an integer
if not minimum <= value <= maximum:
raise ValueError(f'must be between {minimum} and {maximum}')
return value # good value: we leave the loop
except ValueError as err: # both cases end up here
print(f' Error: {err}')
def ask_float(prompt, minimum, maximum):
while True:
try:
value = float(read_line(prompt))
if not minimum <= value <= maximum:
raise ValueError(f'must be between {minimum} and {maximum}')
return value
except ValueError as err:
print(f' Error: {err}')
def ask_option(prompt, options):
while True:
try:
option = read_line(prompt).strip().lower() # ' In-person' -> 'in-person'
if option not in options:
raise ValueError(f'choose one of these: {", ".join(options)}')
return option
except ValueError as err:
print(f' Error: {err}')
print('=== ENROLMENT FORM ===')
name = read_line('Name: ').strip() or 'No name'
age = ask_int('Age (16-99): ', 16, 99)
grade = ask_float('Entry grade (5-14): ', 5.0, 14.0)
mode = ask_option('Mode (in-person/online): ', ['in-person', 'online'])
print('--- Form saved ---')
print(f'Name: {name}')
print(f'Age: {age} years')
print(f'Grade: {grade:.2f}')
print(f'Mode: {mode}')
Let’s zoom in on the function that does the heavy lifting:
def ask_int(prompt, minimum, maximum):
while True: # keeps asking until the value is valid
try:
value = int(read_line(prompt)) # ValueError if it is not an integer
if not minimum <= value <= maximum:
raise ValueError(f'must be between {minimum} and {maximum}')
return value # good value: we leave the loop
except ValueError as err: # both cases end up here
print(f' Error: {err}')
The while True has only one exit: return value, which is reached only if both previous checks pass. int(read_line(prompt)) raises ValueError if the text is not an integer; the chained comparison minimum <= value <= maximum checks the range and, if it fails, raise ValueError(...) throws ours. Both fall into except ValueError as err and err is printed, which is each one’s message.
Output
=== ENROLMENT FORM === Name: Laura Pérez Age (16-99): twelve Error: invalid literal for int() with base 10: 'twelve' Age (16-99): 12 Error: must be between 16 and 99 Age (16-99): 22 Entry grade (5-14): 15 Error: must be between 5.0 and 14.0 Entry grade (5-14): 12,5 Error: could not convert string to float: '12,5' Entry grade (5-14): 12.5 Mode (in-person/online): virtual Error: choose one of these: in-person, online Mode (in-person/online): In-person --- Form saved --- Name: Laura Pérez Age: 22 years Grade: 12.50 Mode: in-person
Let’s check every lap of the age loop with a trace:
| Lap | Typed | int() | 16 ≤ value ≤ 99? | What happens |
|---|---|---|---|---|
| 1 | 'twelve' | raises ValueError | (not reached) | Python’s message, another lap |
| 2 | '12' | 12 | No: 12 < 16 | Our raise, another lap |
| 3 | '22' | 22 | Yes | return 22, leaves the loop |
The grade follows the same pattern with float(): 15 is above 14, 12,5 is not a number for Python (it uses a decimal point) and 12.5 gets in. The mode accepts In-person because .strip().lower() turns it into in-person before checking.
The diagram is the same loop seen from above: two red arrows going back to read_line() and a single green exit.
Questions you might ask
Why raise a ValueError for the range when I could use a print and a continue? It would work, but you would have two places handling errors. With the raise, everything that makes the value invalid goes through the same except and the loop is shorter. And if tomorrow you want to log the failures to a file, you change one line.
Why is the try inside the while and not outside? If the try wrapped the whole loop, the first error would take you out of the loop and the value would never be asked for again. Inside, each lap has its own try: a failure only spoils that lap.
Why is the message for 'twelve' so technical? Because it is Python’s (invalid literal for int() with base 10). We print err as it is. That is what happened to my classmate with 12,5. In “Try it yourself” you replace it with a friendlier one.
Why does ask_option have a try if nothing in it raises Python exceptions? Because our own raise ValueError does. A try catches Python’s exceptions and yours alike: to it they are all the same.
What if the name arrives empty? read_line('Name: ').strip() would give '', which is falsy, and or 'No name' sets a default. No exception is needed here: nothing “fails”, a value is just missing.
Putting return value outside the try, after the except. If int() fails, value never gets created and the return would raise UnboundLocalError. Return the value inside the try, right when you know it is good.
Try it yourself
- Make
ask_floataccept the comma: replacefloat(read_line(prompt))withfloat(read_line(prompt).replace(',', '.'))and check that12,5gets in at the first go. - Split the two messages: catch
int()‘sValueErrorin an innertryto print “the age must be a whole number”, and keep the rangeraisewith its own message.
Python exceptions practice — Program 2: Robust calculator
A one-line calculator: you type 10 / 3 and it answers = 3.333…, until you type exit. What makes it tricky is that anything can arrive: numbers that aren’t numbers, operators that don’t exist, half lines, divisions by zero and results so big they don’t fit in a float. Each error must come out with its own message and the calculator must keep working.
The idea
We split the responsibilities. calculate(line) only calculates: it splits the line, checks the format and the operator and, if something is off, raises. It prints nothing. The main loop is the only one that catches, with one except per error family. The operations live in a dictionary of functions, so we don’t need one if per operator.
Before programming, write down which errors Python raises on its own and which ones you have to raise. Dividing by zero or float('ten') are raised by Python. That the line has three pieces or that ^ is not a valid operator are not: those are your rules.
The code
# What the person would type (on your computer, replace read_line() with input()):
TYPED = iter(['10 / 0', 'ten + 3', '10 ^ 3', '8 //', '2 ** 10000', '7 % 0', '2 ** 10', 'exit'])
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
OPERATIONS = { # symbol -> function that does the sum
'+': lambda a, b: a + b,
'-': lambda a, b: a - b,
'*': lambda a, b: a * b,
'/': lambda a, b: a / b,
'//': lambda a, b: a // b,
'%': lambda a, b: a % b,
'**': lambda a, b: a ** b,
}
def calculate(line):
parts = line.split()
if len(parts) != 3: # '8 //' only has two pieces
raise ValueError('format: number operator number')
a, op, b = parts
if op not in OPERATIONS:
raise ValueError(f'unknown operator: {op}')
return OPERATIONS[op](float(a), float(b)) # float() can fail too
print('=== CALCULATOR (type "exit" to quit) ===')
while True:
line = read_line('> ')
if line == 'exit':
print('See you')
break
try:
print(' =', calculate(line))
except ZeroDivisionError as err: # raised by Python, not by us
print(' Math error:', err)
except OverflowError as err: # result too big for a float
print(' Too large:', err)
except ValueError as err: # our raises and float()'s
print(' Error:', err)
The function that decides whether the line is valid:
def calculate(line):
parts = line.split()
if len(parts) != 3: # '8 //' only has two pieces
raise ValueError('format: number operator number')
a, op, b = parts
if op not in OPERATIONS:
raise ValueError(f'unknown operator: {op}')
return OPERATIONS[op](float(a), float(b)) # float() can fail too
line.split() cuts at the spaces: '10 / 0' gives ['10', '/', '0']. If there aren’t three pieces, raise ValueError. If the operator is not in OPERATIONS, another raise ValueError. And the last line does three things that can fail: float(a), float(b) and the operation itself. None of them is protected here, on purpose: they climb up to the loop.
Output
=== CALCULATOR (type "exit" to quit) === > 10 / 0 Math error: float division by zero > ten + 3 Error: could not convert string to float: 'ten' > 10 ^ 3 Error: unknown operator: ^ > 8 // Error: format: number operator number > 2 ** 10000 Too large: (34, 'Numerical result out of range') > 7 % 0 Math error: float modulo by zero > 2 ** 10 = 1024.0 > exit See you
Each line ends in a different except:
| Typed | Who raises | Exception | except that catches it |
|---|---|---|---|
10 / 0 | Python, in a / b | ZeroDivisionError | Math error |
ten + 3 | Python, in float('ten') | ValueError | Error |
10 ^ 3 | Us: unknown operator | ValueError | Error |
8 // | Us: two pieces | ValueError | Error |
2 ** 10000 | Python: does not fit in a float | OverflowError | Too large |
7 % 0 | Python, in a % b | ZeroDivisionError | Math error |
2 ** 10 | nobody | none | Result: 2¹⁰ = 1024.0 |
Notice the ZeroDivisionError messages: they change with the operation (float division by zero, float modulo by zero). Python writes them, which is why we print them with err instead of inventing our own.
Questions you might ask
Why don’t we check b == 0 before dividing, as the old article did? Because Python already raises ZeroDivisionError on its own, with a correct message, for /, // and %. Repeating that check by hand is code that can get it wrong (forgetting %, say) without adding anything.
Why does OverflowError have its own except instead of going with ZeroDivisionError? Both are children of ArithmeticError and we could catch them together with except ArithmeticError, but the message for the user is different: “you can’t divide by zero” is not the same as “the number is too big”.
Why doesn’t calculate have any try? Because it doesn’t know what to do with the error: printing it is the loop’s job. If tomorrow you use calculate from a graphical interface, you will want to show the error in a window, not in the terminal. The function raises; whoever uses it decides.
Does the order of the except clauses matter here? Not between these three, because none is the parent of another (ValueError is not a child of ArithmeticError). But if you added except ArithmeticError before except ZeroDivisionError, the latter would never run.
Putting the result print and calculate in the same try together with more code (reading, checking exit…). The more lines in the try, the harder it is to know what raised the ValueError. Here the try has a single line.
Try it yourself
- Add square roots with
'sqrt'as a one-number operator (sqrt 16): you will need to accept two pieces for that case and raiseValueErrorif the number is negative. - Also catch
KeyboardInterruptin the loop so that Ctrl+C prints “See you” instead of the Traceback.
Python exceptions practice — Program 3: Robust file reader
The admin office keeps a group’s grades in a text file, one per line. The real file is never clean: there are empty lines, someone wrote eight in letters and there is an 11 that is not a valid grade. On top of that, the file may not exist (the 2027 records aren’t there yet). The program must read what it can, warn about what it can’t and, whatever happens, close the file.
The idea
Two levels of try. The outer one wraps all the work with the file: it catches the file not existing (FileNotFoundError) or not having permission (PermissionError) and, in its finally, closes it. The inner one goes inside the loop and wraps only the conversion of each line: if a line is bad, it warns and moves on to the next, without interrupting the reading. So that the finally can ask whether there is anything to close, file starts as None.
Before programming, separate the errors that affect the whole file (it doesn’t exist) from those that affect a single line ('eight'). Each group goes in a try at its own level.
The code
# We create the sample file (in the admin office it would already exist)
with open('grades.txt', 'w', encoding='utf-8') as f:
f.write('8.5\neight\n11\n6\n\n9.5\n4\n7\n')
def read_grades(filename):
file = None # so it exists even if open() fails
try:
file = open(filename, encoding='utf-8')
grades = []
for num, line in enumerate(file, start=1):
line = line.strip()
if not line:
continue # empty line: neither an error nor a grade
try: # try INSIDE the loop: a bad line does not stop the reading
grade = float(line)
except ValueError:
print(f' Line {num} ignored: {line!r} is not a number')
continue
if not 0 <= grade <= 10:
print(f' Line {num} ignored: {grade} is out of range')
continue
grades.append(grade)
return grades
except FileNotFoundError:
print(f' Error: file {filename!r} does not exist')
return []
except PermissionError:
print(f' Error: no permission to read {filename!r}')
return []
finally: # runs with return, with an error and without one
if file is not None:
file.close()
print(' (file closed)')
def report(grades):
if not grades:
print(' No grades to analyse')
return
passed = sum(1 for n in grades if n >= 5)
print(f' {len(grades)} grades | average {sum(grades) / len(grades):.2f} | '
f'max {max(grades)} | min {min(grades)} | passed {passed}/{len(grades)}')
for name in ['grades.txt', 'grades_2027.txt']:
print('Reading', name)
report(read_grades(name))
The heart of the program is the loop, with its inner try:
for num, line in enumerate(file, start=1):
line = line.strip()
if not line:
continue # empty line: neither an error nor a grade
try: # try INSIDE the loop: a bad line does not stop the reading
grade = float(line)
except ValueError:
print(f' Line {num} ignored: {line!r} is not a number')
continue
if not 0 <= grade <= 10:
print(f' Line {num} ignored: {grade} is out of range')
continue
grades.append(grade)
enumerate(file, start=1) gives the line number (starting at 1, as a person counts) along with its text. The try wraps only float(line): it is the only thing that can fail. The range is checked with a normal if, because nothing needs raising here: warning and moving on is enough.
Output
Reading grades.txt Line 2 ignored: 'eight' is not a number Line 3 ignored: 11.0 is out of range (file closed) 5 grades | average 7.00 | max 9.5 | min 4.0 | passed 4/5 Reading grades_2027.txt Error: file 'grades_2027.txt' does not exist No grades to analyse
Trace of the loop over grades.txt:
| Line | Text | What happens | grades afterwards |
|---|---|---|---|
| 1 | 8.5 | Stored | [8.5] |
| 2 | eight | ValueError, ignored | [8.5] |
| 3 | 11 | Out of range, ignored | [8.5] |
| 4 | 6 | Stored | [8.5, 6.0] |
| 5 | (empty) | continue without warning | [8.5, 6.0] |
| 6 | 9.5 | Stored | [8.5, 6.0, 9.5] |
| 7 | 4 | Stored | [8.5, 6.0, 9.5, 4.0] |
| 8 | 7 | Stored | [8.5, 6.0, 9.5, 4.0, 7.0] |
Check: 8.5 + 6 + 9.5 + 4 + 7 = 35, and 35 / 5 = 7.00. Everyone passes except the 4: 4/5. Notice that “(file closed)” comes out before the report: finally runs when leaving read_grades, just before return grades hands over the list. With grades_2027.txt, open() fails, file is still None and the finally has nothing to close.
Questions you might ask
Why file = None before the try? Because if open() fails, the variable file would never be created and the finally would raise UnboundLocalError when trying file.close(). In the exercises you’ll see it fail for real.
Isn’t it easier to use with open(...)? Yes, and it is what you will do almost always: with closes the file on its own when the block ends, even if there is an exception. Here we write it with finally so you can see what with does underneath. In the contact book exercise we use with.
Why an if for the range and not a raise? Because we are in the same place where the error is handled. Raising an exception to catch it two lines below would be going the long way round. raise makes sense when the one who detects and the one who handles are different functions, as in program 1.
What would happen without the inner try? Line 2 (eight) would raise ValueError, which is neither FileNotFoundError nor PermissionError, so the outer try wouldn’t catch it: the finally would close the file and the program would stop with the Traceback. Five valid grades lost because of one bad line.
Catching except Exception in the outer try “so it never fails”. A bad line would end up there, an empty list would be returned and you would never know the file had five good grades.
Try it yourself
- Rewrite
read_gradeswithwith open(...) as file:and check that the output is the same (apart from the “file closed” message, which is no longer needed). - Count how many lines were ignored and return it along with the grades:
return grades, ignored.
Python exceptions practice — Program 4: Class with custom exceptions
A Rational class for fractions such as 3/4. What makes it tricky is that a fraction with denominator 0 doesn’t exist and that adding it only makes sense with another fraction. We want the class itself to prevent that by raising its own exceptions, organised in a family, and the main program to ask for data until it has two valid fractions and adds them. We use a class because that is how it is done in FP2; if you haven’t seen classes yet, keep the idea and come back after the next topic, Python classes.
The idea
Three exceptions: RationalError as the parent, and ZeroDenominatorError and WrongTypeError as children. The __init__ constructor checks the data and raises the right one; if everything is fine, it stores the fraction simplified with the greatest common divisor (gcd). The main program catches the specific child when it wants a specific message, and the parent when any member of the family will do.
Before programming, draw the exception tree on paper. Once you are clear who is whose parent, the order of the except clauses follows.
The code
# What the person would type (on your computer, replace read_line() with input()):
TYPED = iter(['3', '0', 'three', '3', '4', '1', '2'])
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
from math import gcd
class RationalError(Exception):
"""Base of the rational number errors."""
class ZeroDenominatorError(RationalError):
"""The denominator is 0."""
class WrongTypeError(RationalError):
"""Numerator or denominator are not integers."""
class Rational:
def __init__(self, num, den):
if not (isinstance(num, int) and isinstance(den, int)):
raise WrongTypeError('numerator and denominator must be integers')
if den == 0:
raise ZeroDenominatorError('the denominator cannot be zero')
m = gcd(num, den) # we simplify: 10/8 -> 5/4
self.num, self.den = num // m, den // m
def __str__(self):
return f'{self.num}/{self.den}'
def __add__(self, other): # r1 + r2 calls r1.__add__(r2)
if not isinstance(other, Rational):
raise WrongTypeError('only rationals can be added')
return Rational(self.num * other.den + other.num * self.den, self.den * other.den)
def ask_rational(n):
num = int(read_line(f'Numerator {n}: ')) # ValueError if it is not an integer
den = int(read_line(f'Denominator {n}: '))
return Rational(num, den) # RationalError if it is not valid
print('=== ADDING FRACTIONS ===')
while True:
try:
r1 = ask_rational(1)
r2 = ask_rational(2)
break
except ZeroDenominatorError as err:
print(' Rational error:', err)
except ValueError:
print(' Error: type whole numbers')
print(f'{r1} + {r2} = {r1 + r2}')
try:
print(r1 + 0.5)
except RationalError as err: # the base catches any child
print(f' {type(err).__name__}: {err}')
The constructor is the gatekeeper: no bad fraction ever comes to exist.
def __init__(self, num, den):
if not (isinstance(num, int) and isinstance(den, int)):
raise WrongTypeError('numerator and denominator must be integers')
if den == 0:
raise ZeroDenominatorError('the denominator cannot be zero')
m = gcd(num, den) # we simplify: 10/8 -> 5/4
self.num, self.den = num // m, den // m
If num or den are not integers, WrongTypeError; if den is 0, ZeroDenominatorError. Only if both checks pass is gcd computed and the attributes stored. That way, any Rational in your program is valid: there is no need to check again in every method.
Output
=== ADDING FRACTIONS === Numerator 1: 3 Denominator 1: 0 Rational error: the denominator cannot be zero Numerator 1: three Error: type whole numbers Numerator 1: 3 Denominator 1: 4 Numerator 2: 1 Denominator 2: 2 3/4 + 1/2 = 5/4 WrongTypeError: only rationals can be added
The sum, by hand: 3/4 + 1/2 = (3·2 + 1·4) / (4·2) = 10/8, and since gcd(10, 8) = 2, it becomes 5/4. The old article showed 10/8 because it didn’t simplify.
The diagram follows the first attempt: Rational(3, 0) raises ZeroDenominatorError inside __init__; ask_rational has no try, so the exception goes straight through it and the loop’s except catches it and asks again. The second attempt fails earlier, at int('three'), and except ValueError catches it.
Questions you might ask
Why is the last sum (r1 + 0.5) caught by except RationalError if it raises WrongTypeError? Because WrongTypeError is a child of RationalError. Catching the parent is the way to say “any problem with fractions”, and type(err).__name__ tells us which one it was.
Why don’t we catch WrongTypeError in the loop? Because it can’t happen there: int() always returns an integer or raises ValueError. Catching something that can’t happen only confuses whoever reads the code.
What is the parent class for if we never raise it? For catching as a group. A program using your class can write a single except RationalError and be covered for any fraction error, without knowing every child. And if tomorrow you add a new child, that except already covers it.
And __add__? It is the method Python calls when you write r1 + r2. You’ll see it in depth in the operator overloading topic; here it is enough to know that it can raise exceptions like any other method.
Checking the denominator in the main program instead of in __init__. It works until someone creates a Rational(1, 0) somewhere else. Validation has to live where the object is born.
Try it yourself
- Add
DivisionByZeroError(a child ofRationalError) and a__truediv__method that raises it when dividing by a fraction worth 0. - Make
Rational(2, -4)keep the sign in the numerator (-1/2) and check the output ofprint(Rational(2, -4)).
Common mistakes in this practice
- The
tryoutside the loop: the first error leaves thewhileand the value is never asked for again. - A
returnafter theexcept: if thetryfailed, the variable you return doesn’t exist (UnboundLocalError). - Repeating what Python already checks:
if b == 0: raise ZeroDivisionErroris extra code; Python raises it on its own. - An
except Exceptionin the file reader: one bad line empties the whole reading without telling you why. - Using the file variable in
finallywithout initialising it: ifopen()fails,file.close()blows up. - Catching the parent before the child: the specific
exceptnever runs and nobody warns you.
Watch it step by step in Python Tutor
In Python Tutor (choose Python 3) you can see very clearly how a custom exception travels. Paste this program, a minimal version of program 4:
class RationalError(Exception):
pass
class ZeroDenominatorError(RationalError):
pass
def create(num, den):
if den == 0:
raise ZeroDenominatorError('zero denominator')
return (num, den)
for den in [4, 0]:
try:
r = create(3, den)
print('Created:', r)
except RationalError as err:
print('Caught:', type(err).__name__, err)
finally:
print('Next')
Output:
Created: (3, 4) Next Caught: ZeroDenominatorError zero denominator Next
What to watch:
- At the start both classes are created in the global frame: they are objects like any other.
- In the first lap (
den = 4),createreturns the tuple(3, 4)and the arrow goes fromtrytofinally, without touchingexcept. - In the second lap, at the
raisestep, thecreateframe disappears and the arrow jumps toexcept RationalError, even though the exception is aZeroDenominatorError. - The variable
rstill holds(3, 4)from the previous lap: the second lap’s assignment never finished. - “Next” is printed in both laps: that is the
finally.
Summary and next step
- A loop with a
tryinside and areturnwhen the value is valid is the pattern for asking until the data is right. raisein the function that detects the problem andexceptin the one that knows what to do:calculateraises, the loop catches.- Let Python raise what it already knows how to raise (
ZeroDivisionError,OverflowError) and use its message. - Two levels of
trywith files: the outer for the whole file, the inner for each line. finally(orwith) guarantees the file is closed; initialise the variable toNoneif you usefinally.- Custom exceptions in families let you catch the specific one or the whole group with a single
except.
Checklist: have you practised it?
- Can you explain why the
tryinask_intgoes inside thewhile? - Can you say, for each calculator line, who raises the exception, Python or us?
- Do you understand why “(file closed)” comes out before the report?
- Could you add a new child to the
RationalErrorfamily and catch it? - Have you run the four programs and checked the outputs?
- Have you tried the “Try it yourself” tasks without looking at the theory?
If you answered yes to almost all of them, you are ready for the Python exceptions exercises, where I no longer give you the code. See you there!
Next step: exercisesPython exceptions exercises — a unit converter, passwords with their own family of errors and a contact book that always saves (and the finally that closed a file that didn’t exist)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)