Warm-up · Activity 1 of 7
// I1.1 · ~30 min · Intermediate
Raising and defining your own exceptions
After this lesson you can give your program its own exception types, group them under one base class, and catch exactly the errors you mean to.
Lesson 1 of 5 in I1 Errors in depth
You will be able to
- Define your own exception class based on Exception, and raise it with a clear message
- Group exceptions under a base class and catch the whole family with one except clause
- Order except clauses from specific to general, and catch no more than you mean to
Predict · Activity 2 of 7
Predict before you read on: OutOfStockError is raised, but the except clause names ShopError. What does this print?
class ShopError(Exception): pass class OutOfStockError(ShopError): pass try: raise OutOfStockError("no pears left") except ShopError as err: print("shop problem:", err)Practice · Activity 3 of 7
Fill in the base class, so that TemperatureError is an ordinary exception that except Exception catches.
class TemperatureError(____): passclass TemperatureError():Practice · Activity 4 of 7
ConfigError is derived from AppError. What does this print?
class AppError(Exception): pass class ConfigError(AppError): pass try: raise ConfigError("missing key: port") except AppError: print("app error") except ConfigError: print("config error")Practice · Activity 5 of 7
Python’s own exceptions form a hierarchy too. Match each exception to its direct base class.
Brain teaser · Activity 6 of 7
Brain teaser. Two exceptions go through the same try statement. What does this print?
class AppError(Exception): pass class ConfigError(AppError): pass log = [] for exc in [ConfigError("bad port"), AppError("disk full")]: try: raise exc except ConfigError: log.append("config") except AppError as err: log.append(f"app: {err}") print(log)Apply · Activity 7 of 7
Mini-task. A thermostat accepts settings from 5 to 30 degrees. Define TemperatureError and two subclasses, TooColdError and TooHotError. Write check_setting(celsius), which raises the right one with a message. Then try 4, 21 and 35 in a loop with a single except clause, printing the class name and the message of each rejection.
Check your work against this list
Build it yourself
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
A bank with its own errors
A small bank module defines BankError as the base of its errors, and two specific ones. withdraw raises InsufficientFundsError or AccountLockedError, and a missing account raises the built-in KeyError. The loop gives the most specific error its own message, handles every other bank error with the base class, and keeps KeyError apart.
main.py
class BankError(Exception):
"""Base class for every error this module raises."""
class InsufficientFundsError(BankError):
pass
class AccountLockedError(BankError):
pass
LOCKED = {"bob"}
def withdraw(balances: dict[str, float], name: str, amount: float) -> float:
if name in LOCKED:
raise AccountLockedError(f"{name}'s account is locked")
balance = balances[name] # KeyError for an unknown name
if amount > balance:
raise InsufficientFundsError(f"{name} has {balance:.2f}, needs {amount:.2f}")
balances[name] = balance - amount
return balances[name]
balances = {"ada": 50.0, "bob": 20.0}
requests = [("ada", 20.0), ("ada", 40.0), ("bob", 5.0), ("eve", 1.0)]
for name, amount in requests:
try:
left = withdraw(balances, name, amount)
except InsufficientFundsError as err:
print("declined:", err)
except BankError as err:
print(f"bank error ({type(err).__name__}):", err)
except KeyError:
print("unknown account:", name)
else:
print(f"{name} withdrew {amount:.2f}, {left:.2f} left")
Run it with
python main.pyOutput
ada withdrew 20.00, 30.00 left
declined: ada has 30.00, needs 40.00
bank error (AccountLockedError): bob's account is locked
unknown account: eve- The docstring alone is a valid class body, like pass.
- InsufficientFundsError has its own clause above BankError; the other way round it could never run.
- except BankError catches AccountLockedError, and type(err).__name__ still names the real class.
- KeyError is not a BankError, so it needs its own clause.
Change it and run it
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Exercises
Exercise 1 of 2
A validation error of your own
Define ValidationError, derived from Exception. Then write validate_age(text), which returns the age as an int. For text that is not a whole number, raise ValidationError with the message not a number: followed by the text in repr form, for example not a number: 'ten'. For an age below 0 or above 150, raise ValidationError("age out of range: 200"), with the real value.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
int(text) raises ValueError for text such as "ten". Catch it with try and except ValueError.
Hint 2
Inside that except clause, raise your own error: raise ValidationError(f"not a number: {text!r}").
Hint 3
After the try statement, check the range with if not 0 <= age <= 150: and raise ValidationError(f"age out of range: {age}").
Show a solution
One way to solve it. Yours can look different and still pass the checks.
class ValidationError(Exception):
pass
def validate_age(text: str) -> int:
try:
age = int(text)
except ValueError:
raise ValidationError(f"not a number: {text!r}")
if not 0 <= age <= 150:
raise ValidationError(f"age out of range: {age}")
return age
Run it on your computer
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
class ValidationError(Exception):
pass
def validate_age(text: str) -> int:
return int(text)
test_main.py
from main import ValidationError, validate_age
def test_is_exception():
"""ValidationError is derived from Exception"""
assert issubclass(ValidationError, Exception), "ValidationError must be derived from Exception"
def test_valid():
"""A valid age comes back as an int"""
got = validate_age("42")
assert got == 42, f"validate_age('42') returned {got!r}, expected 42"
def test_not_a_number():
"""Text that is not a number raises ValidationError"""
try:
validate_age("ten")
except ValidationError as err:
assert str(err) == "not a number: 'ten'", f"the message was {str(err)!r}"
except ValueError:
assert False, "validate_age raised ValueError; raise ValidationError instead"
else:
assert False, "validate_age('ten') returned normally"
def test_out_of_range():
"""200 and -1 are out of range"""
for text in ["200", "-1"]:
try:
validate_age(text)
except ValidationError as err:
want = f"age out of range: {int(text)}"
assert str(err) == want, f"for {text!r} the message was {str(err)!r}, expected {want!r}"
else:
assert False, f"validate_age({text!r}) returned normally"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 2 of 2
A family of config errors
read_port already raises MissingKeyError and BadValueError. Make both subclasses of ConfigError. Then fix describe(settings): it returns "port " plus the port, or "config problem: " plus the message for any ConfigError, using a single except clause. Other errors must not be caught.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
The base class goes in the parentheses: class MissingKeyError(ConfigError):. Do the same for BadValueError.
Hint 2
In describe, one clause for the whole family: except ConfigError as err:.
Hint 3
Do not use except Exception: the last test passes a number instead of a string, and that AttributeError must go through.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
class ConfigError(Exception):
pass
class MissingKeyError(ConfigError):
pass
class BadValueError(ConfigError):
pass
def read_port(settings: dict[str, str]) -> int:
if "port" not in settings:
raise MissingKeyError("missing key: port")
text = settings["port"]
if not text.isdigit() or not 1 <= int(text) <= 65535:
raise BadValueError(f"port must be 1-65535, got {text!r}")
return int(text)
def describe(settings: dict[str, str]) -> str:
try:
port = read_port(settings)
except ConfigError as err:
return f"config problem: {err}"
return f"port {port}"
Run it on your computer
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
class ConfigError(Exception):
pass
class MissingKeyError(Exception):
pass
class BadValueError(Exception):
pass
def read_port(settings: dict[str, str]) -> int:
if "port" not in settings:
raise MissingKeyError("missing key: port")
text = settings["port"]
if not text.isdigit() or not 1 <= int(text) <= 65535:
raise BadValueError(f"port must be 1-65535, got {text!r}")
return int(text)
def describe(settings: dict[str, str]) -> str:
try:
port = read_port(settings)
except MissingKeyError as err:
return f"config problem: {err}"
return f"port {port}"
test_main.py
from main import BadValueError, ConfigError, MissingKeyError, describe
def test_hierarchy():
"""Both errors are subclasses of ConfigError"""
assert issubclass(MissingKeyError, ConfigError), "MissingKeyError must be derived from ConfigError"
assert issubclass(BadValueError, ConfigError), "BadValueError must be derived from ConfigError"
def test_port():
"""A good port is described"""
got = describe({"port": "8080"})
assert got == "port 8080", f"describe returned {got!r}"
def test_missing():
"""A missing port is a config problem"""
got = describe({})
assert got == "config problem: missing key: port", f"describe returned {got!r}"
def test_bad_value():
"""A bad port is a config problem too"""
got = describe({"port": "99999"})
assert got == "config problem: port must be 1-65535, got '99999'", f"describe returned {got!r}"
def test_other_errors_pass():
"""Errors that are not ConfigErrors are not caught"""
try:
describe({"port": 8080}) # type: ignore[dict-item]
except AttributeError:
pass
else:
assert False, "describe caught an AttributeError; catch only ConfigError"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyCommon mistakes
An exception class without a base class
class StockError:
pass
raise StockError("no pears left")
What Python prints
TypeError: StockError() takes no argumentsWhy, and the fix
Without a base class, StockError is an ordinary class, not an exception. It does not even accept a message, and raise would reject it too. Name the base class: class StockError(Exception):. Then it takes a message, and raise and except work with it like with any built-in exception.
Expecting a subclass clause to catch the base class
class AppError(Exception):
pass
class ConfigError(AppError):
pass
try:
raise AppError("disk full")
except ConfigError:
print("config problem")
What Python prints
AppError: disk fullWhy, and the fix
except ConfigError catches ConfigError and classes derived from it. AppError is its base class, not a subclass, so the error goes through uncaught. To handle every error of the family, catch the base class: except AppError:.
Using a base class before it is defined
class ConfigError(AppError):
pass
class AppError(Exception):
pass
What Python prints
NameError: name 'AppError' is not definedWhy, and the fix
A class statement runs from top to bottom like any other line, and the base class in the parentheses must already exist. Define the base class first, then the classes derived from it.
Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source
Exit ticket
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.