Skip to content

    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:

    basico/cap23_excecoes.pylinha 10
    print([c.__name__ for c in ZeroDivisionError.__mro__])
    
    Output
    ['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

    basico/cap23_excecoes.pylinhas 15 a 29
    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"))
    
    Output
    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 with return or 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:

    basico/cap23_excecoes.pylinhas 34 a 47
    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__)
    
    Output
    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:

    basico/cap23_excecoes.pylinhas 52 a 68
    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)
    
    Output
    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:

    basico/cap23_excecoes.pylinhas 73 a 85
    config = {"porta": 8080}
    
    if "host" in config:
        host = config["host"]
    else:
        host = "localhost"
    
    try:
        host = config["host"]
    except KeyError:
        host = "localhost"
    
    print(host)
    
    Output
    localhost
    

    assert is not input validation

    The assert is used to declare what needs to be true in your code, like a sanity check for developers. It can be turned off with the -O interpreter option. To validate external data (user input, files, network), use if and raise.

    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.