Skip to content

    Chapter 18, Basic

    Scope and function pitfalls

    Where a variable exists, and the two traps that cause more people to waste time: poorly used local variable and mutable default value.

    Code for this chapter: basico/cap18_escopo.py

    Scope: where a name is valid

    Python looks for a name in four places, in this order, which the community calls LEGB: Local (inside the function), Enclosing (outer function), Global (module) and Built-in (built-in names, like len).

    basico/cap18_escopo.pylinhas 10 a 24
    mensagem = "global"
    
    
    def externa():
        mensagem = "local da externa"
    
        def interna():
            print(mensagem)
    
        interna()
        print(mensagem)
    
    
    externa()
    print(mensagem)
    
    Output
    local da externa
    local da externa
    global
    

    To modify a global variable inside a function, you need to declare global. I avoid it: global state makes the code hard to test and understand. Prefer to receive the value as a parameter and return the new one:

    basico/cap18_escopo.pylinhas 26 a 36
    contador = 0
    
    
    def incrementar():
        global contador
        contador += 1
    
    
    incrementar()
    incrementar()
    print(contador)
    
    Output
    2
    

    UnboundLocalError

    If you assign to a name inside the function, Python treats it as local throughout the entire function, including in the lines before the assignment. That's why the error below occurs even with a total global:

    basico/cap18_escopo.pylinhas 41 a 51
    total = 10
    
    
    def quebrado():
        total += 1
    
    
    try:
        quebrado()
    except UnboundLocalError as erro:
        print(erro)
    
    Output
    cannot access local variable 'total' where it is not associated with a value
    

    The mutable default value

    This is the most famous trap in the language. The default value of a parameter is created only once, when the function is defined. If it's a list, all calls share the same list:

    basico/cap18_escopo.pylinhas 56 a 63
    def adicionar(item, lista=[]):
        lista.append(item)
        return lista
    
    
    print(adicionar(1))
    print(adicionar(2))
    print(adicionar.__defaults__)
    
    Output
    [1]
    [1, 2]
    ([1, 2],)
    

    The second call returns [1, 2], even though you didn't pass any list: the 1 was stored in its own default value. The solution is to use None as a sentinel and create the list inside the function:

    basico/cap18_escopo.pylinhas 65 a 73
    def adicionar_correto(item, lista=None):
        if lista is None:
            lista = []
        lista.append(item)
        return lista
    
    
    print(adicionar_correto(1))
    print(adicionar_correto(2))
    
    Output
    [1]
    [2]
    

    The rule I follow: is never use list, dictionary, or set as a default value. Linters like ruff automatically flag this error (rule B006).

    Side effects

    A function that alters the arguments it receives has a side effect. This is not prohibited, but it should be explicit in the name and documentation. Compare:

    basico/cap18_escopo.pylinhas 78 a 90
    def ordenar_no_lugar(lista):
        lista.sort()
    
    
    def ordenada(lista):
        return sorted(lista)
    
    
    original = [3, 1, 2]
    nova = ordenada(original)
    print(original, nova)
    ordenar_no_lugar(original)
    print(original)
    
    Output
    [3, 1, 2] [1, 2, 3]
    [1, 2, 3]
    

    Recursion

    A function can call itself. Every recursive function needs a base case that stops the recursion. Python limits the depth (1000 calls by default) and does not optimizes tail recursion, so for deep problems prefer a loop:

    basico/cap18_escopo.pylinhas 95 a 113
    import sys
    
    
    def fatorial(n):
        return 1 if n <= 1 else n * fatorial(n - 1)
    
    
    print(fatorial(5))
    print(sys.getrecursionlimit())
    
    
    def sem_fim(n):
        return sem_fim(n + 1)
    
    
    try:
        sem_fim(0)
    except RecursionError:
        print("limite de recursão atingido")
    
    Output
    120
    1000
    limite de recursão atingido
    

    Exercise 1

    Fix the default value bug

    The function below should return a new list on each call without an argument. Fix it.

    Texto
    def registrar(evento, historico=[]):
        historico.append(evento)
        return historico