Chapter 23, Basic
Exceptions
Errors will happen: missing file, network down, invalid input. What defines a good program is what it does when they occur.
Code for this chapter: basico/cap23_excecoes.py
Exceptions are objects organized in a hierarchy
A runtime error is represented by an exception, which is an instance of a class. All descend from BaseException, and the ones you deal with daily descend from Exception:
print([c.__name__ for c in ZeroDivisionError.__mro__])
['ZeroDivisionError', 'ArithmeticError', 'Exception', 'BaseException', 'object']
This matters because except ArithmeticError captures ZeroDivisionError too: capturing a class captures the subclasses. Even syntax errors are part of the hierarchy (SyntaxError descends from Exception), although they appear at compile time, before the program runs.
try, except, else and finally
def converter(texto):
try:
valor = int(texto)
except ValueError:
print(f"'{texto}' não é um inteiro")
return None
else:
print("conversão feita")
return valor
finally:
print("fim da tentativa")
print(converter("42"))
print(converter("abc"))
conversão feita
fim da tentativa
42
'abc' não é um inteiro
fim da tentativa
None
try: the code that can fail.except: what to do if that exception occurs.else: runs only if does not there was an exception.finally: runs always, even withreturnor error. It is the place for cleanup.
Capture only what you know how to handle
I follow three rules. First: catch specificexceptions, never a except: blank, which swallows even the Ctrl+C. Second: the block try should be as small as possible. Third: when you need to translate an exception, use raise ... from, which preserves the original cause:
def dividir(a, b):
try:
return a / b
except ZeroDivisionError:
return float("inf")
except TypeError as erro:
raise ValueError("os dois valores devem ser números") from erro
print(dividir(1, 0))
try:
dividir(1, "a")
except ValueError as erro:
print(erro, "|", type(erro.__cause__).__name__)
inf
os dois valores devem ser números | TypeError
Custom exceptions
When your domain has a named failure (insufficient balance, non-existent order), create a class. Those using your code catch by name, without relying on message text. Classes are the subject of the intermediate level, so for now just copy the mold:
class SaldoInsuficiente(Exception):
def __init__(self, saldo, valor):
super().__init__(f"saldo {saldo} insuficiente para sacar {valor}")
self.saldo = saldo
self.valor = valor
def sacar(saldo, valor):
if valor > saldo:
raise SaldoInsuficiente(saldo, valor)
return saldo - valor
try:
sacar(100, 150)
except SaldoInsuficiente as erro:
print(erro, erro.valor - erro.saldo)
saldo 100 insuficiente para sacar 150 50
Asking for forgiveness or asking for permission
There are two styles. LBYL (look before you leap) checks before acting. EAFP (easier to ask forgiveness than permission) tries and handles the failure. The EAFP style is the most common in Python, and it is safer in concurrent scenarios, because between "check" and "act" the world can change:
config = {"porta": 8080}
if "host" in config:
host = config["host"]
else:
host = "localhost"
try:
host = config["host"]
except KeyError:
host = "localhost"
print(host)
localhost
assert is not input validation
The
assertis used to declare what needs to be true in your code, like a sanity check for developers. It can be turned off with the-Ointerpreter option. To validate external data (user input, files, network), useifandraise.
Exercise 1
Safe division
Write dividir_seguro(a, b) that returns None when b it is zero.
Exercise 2
Validate an age
Write ler_idade(texto) that returns an integer and raises ValueError with a clear message if the text is not a number or if the value is negative.