Vai al contenuto principale

Ambito delle variabili in Python e regola LEGB spiegati

Scopri cosa sono gli ambiti delle variabili e familiarizza con la regola "LEGB". Vedrai anche all’opera le keyword global e nonlocal.
· 8 min leggi

Scopri con l'IA

Vedere subito dopo poche righe messaggi come “name is not defined” o “local variable referenced before assignment” è frustrante. Il codice sembra a posto, ma Python non è d'accordo. La causa è quasi sempre l’ambito (scope): dove un nome è visibile e a quale oggetto si riferisce in quel momento.

Una volta che capisci come Python risolve i nomi—spesso riassunto nella regola LEGB—questi errori diventano prevedibili e facili da correggere. In questo tutorial spiegherò variabili e ambiti in Python passo dopo passo.

Che cos’è una variabile?

Una variabile è un nome associato a un oggetto. In Python, crei un’associazione con l’operatore di assegnazione =. I nomi non sono tipizzati: possono puntare a qualsiasi oggetto (stringa, int, lista, funzione, ecc.).

customer_name = "Ava Torres"
order_count = 1
total_cents = 1 + 2 * 300  # evaluated first, then bound

Segui queste regole e convenzioni quando assegni i nomi alle variabili per evitare errori di sintassi e bug sottili.

  • Usa solo lettere, cifre e underscore; i nomi non possono iniziare con una cifra.
  • Non usare parole chiave come True, for o class come nomi.
  • Evita di oscurare i built-in come list, dict, sum o max.
  • Preferisci lo snake_case per la leggibilità (PEP 8).

Questi esempi mostrano cosa non fare e gli errori che Python solleva:

first string value = "First string"  # spaces not allowed
# SyntaxError: invalid syntax

1st_value = 10  # cannot start with a digit
# SyntaxError: invalid decimal literal

True = "yes"  # cannot assign to a keyword
# SyntaxError: cannot assign to True

Come funziona l’ambito in Python (LEGB)

Lo scope è la porzione di codice in cui un nome è visibile. Quando usi un nome, Python lo cerca in questo ordine (LEGB):

  • Locale: lo scope della funzione corrente.
  • Enclosing (contenitore): gli scope delle funzioni esterne (per funzioni annidate).
  • Globale: il livello superiore del modulo (namespace del modulo).
  • Built-in: i nomi definiti da Python in builtins (ad esempio, len, print).

I nomi vivono in namespace (pensa a dizionari che mappano nomi a oggetti). Lo scope riguarda quali namespace Python consulta per risolvere un nome in un dato punto del codice.

Scope locale

I nomi assegnati dentro una funzione sono locali a quella funzione salvo diversa dichiarazione. Non sono visibili fuori dalla funzione.

def show_order_id():
    order_id = 42
    print("inside function:", order_id)

show_order_id()
print("outside function:", order_id)  # NameError

Python decide lo scope in fase di compilazione. Se una funzione assegna a un nome in qualsiasi punto del suo corpo, tutti gli usi di quel nome nella funzione sono trattati come locali—portando a un comune UnboundLocalError quando lo leggi prima di assegnarlo:

discount_rate = 0.10  # module-level (global)

def price_with_discount(amount_cents):
    print("configured discount:", discount_rate)  # looks local because of the assignment below
    discount_rate = 0.20  # assignment makes 'discount_rate' local in this function
    return int(amount_cents * (1 - discount_rate))

# UnboundLocalError: cannot access local variable 'discount_rate' where it is not associated with a value

Per usare il nome a livello di modulo nella funzione, evita di assegnargli oppure contrassegnalo come global (vedi sotto).

Scope contenitore (closure)

Le funzioni annidate possono vedere i nomi della loro funzione immediatamente esterna. Per riassegnare un nome del genere (non solo leggerlo), dichiaralo nonlocal.

def make_step_counter():
    count = 0  # enclosing scope for 'increment'

    def increment():
        nonlocal count  # rebind the 'count' in the nearest enclosing function
        count += 1
        return count

    return increment

step = make_step_counter()
print(step())  # 1
print(step())  # 2

Senza nonlocal, assegnare a count dentro increment() creerebbe un nuovo nome locale lasciando invariato il count esterno.

Scope globale

I nomi assegnati al livello superiore di un modulo vivono nel namespace globale del modulo. Qualsiasi funzione può leggerli. Per assegnare a un nome a livello di modulo da dentro una funzione, dichiaralo global.

greeting = "Hello"

def greet_city(city_name):
    print(greeting, city_name)  # reads global

def set_greeting(new_greeting):
    global greeting
    greeting = new_greeting     # rebinds global

greet_city("Nairobi")  # Hello Nairobi
set_greeting("Hi")
greet_city("Nairobi")  # Hi Nairobi

Usa global con parsimonia. Meglio passare valori come parametri e restituire risultati per mantenere il codice testabile e prevedibile.

Scope built-in (e una nota sulle parole chiave)

Lo scope built-in contiene nomi come len, print ed Exception. Evita di oscurarli, altrimenti perderai accesso al built-in in quello scope.

list = [1, 2, 3]     # shadows the built-in 'list' constructor
list("abc")          # TypeError: 'list' object is not callable
del list             # fix by deleting the shadowing name

Le parole chiave (come if, for, def) fanno parte della sintassi di Python, non del namespace built-in, e non possono mai essere usate come identificatori.

Blocchi, loop e comprehension

I costrutti di blocco e le comprehension di Python hanno comportamenti di scope specifici che spesso sorprendono chi arriva da altri linguaggi.

Non esiste uno scope di blocco per if/for/while/with

Le assegnazioni dentro questi blocchi influiscono sullo scope contenitore (funzione o modulo). Le variabili di loop restano definite anche dopo la fine del ciclo.

if True:
    status = "ready"
print(status)  # "ready"

for i in range(3):
    pass
print(i)  # 2 (the last value from the loop)

Le comprehension isolano la loro variabile di iterazione

Le list, dict e set comprehension hanno un proprio scope locale per le variabili di loop. La variabile di iterazione non “fuoriesce” nello scope circostante.

numbers = [1, 2, 3]
[x for x in numbers]
print("x" in globals() or "x" in locals())  # False

Python 3.12 (PEP 709) ha inlined le comprehension per velocità mantenendo questa isolazione; ottieni ancora variabili di loop chiare, non “perdenti”, con esecuzione più rapida.

Uso della keyword global

Dichiara un nome come global dentro una funzione quando devi riassegnare una variabile a livello di modulo. Metti la dichiarazione vicino all’inizio della funzione così è chiaro quale variabile stai modificando.

tax_rate = 0.08

def configure_tax(rate):
    global tax_rate
    tax_rate = float(rate)

def total_with_tax(cents):
    return int(cents * (1 + tax_rate))

configure_tax(0.10)
print(total_with_tax(1000))  # 1100

Uso della keyword nonlocal

Usa nonlocal per riassegnare un nome dallo scope della funzione contenitore più vicino. È comune nelle closure che mantengono stato.

def make_accumulator(start=0):
    total = start
    def add(amount):
        nonlocal total
        total += amount
        return total
    return add

acc = make_accumulator()
print(acc(5))   # 5
print(acc(10))  # 15

Usare nonlocal per un nome non definito in una funzione contenitore è un SyntaxError. Se il nome è globale, usa global.

locals() e globals() nel 2025

Queste funzioni sono utili per ispezione e debug, non per aggiornare variabili. A partire da Python 3.13 (PEP 667), ogni chiamata a

locals()

in una funzione restituisce un’istantanea indipendente. Modificare quell’istantanea non cambia le variabili locali reali. Ho confermato questo comportamento eseguendo il seguente snippet su Python 3.13:

def probe():
    project = "alpha"
    snap1 = locals()
    snap1["project"] = "beta"  # edits the snapshot only
    observed = project         # still "alpha"
    snap2 = locals()           # new snapshot
    return snap1["project"], observed, snap2["project"]

print(probe())  # ('beta', 'alpha', 'alpha')

Usa globals() in modo analogo per il namespace del modulo. Quando scrivi codice applicativo, preferisci parametri espliciti e valori di ritorno rispetto a lookup dinamici.

Errori comuni e come risolverli

Questi errori spiegano la maggior parte dei problemi legati allo scope che vedo in pratica.

  • UnboundLocalError dovuto a un’assegnazione in una funzione: Python tratta un nome come locale se viene assegnato in qualsiasi punto della funzione. Sposta la lettura dopo l’assegnazione, rinomina, oppure aggiungi global/nonlocal a seconda dei casi.
  • Aspettarsi uno scope di blocco: I nomi assegnati in if/for/while/with persistono nello scope contenitore. Usa funzioni di supporto più piccole per limitare lo scope.
  • Oscurare i built-in: Evita nomi come list, dict, sum, id o min. Scegli nomi descrittivi come customer_list o min_allowed.
  • Dimenticare nonlocal nelle closure: Se intendi aggiornare una variabile della funzione esterna, dichiarala nonlocal. Altrimenti crei un nuovo locale e il valore esterno non cambia.
  • Confondere parole chiave e built-in: Le parole chiave (sintassi) non possono mai essere usate come nomi. I built-in possono essere oscurati ma non dovrebbero esserlo.

Buone pratiche per mantenere semplice lo scope

Queste abitudini rendono il codice più facile da leggere, testare e fare debug.

  • Preferisci funzioni piccole e mirate. Definisci le variabili nello scope più ristretto possibile.
  • Passa i dati dentro e fuori tramite parametri e valori di ritorno. Riduci al minimo lo stato a livello di modulo.
  • Usa le closure in modo intenzionale. Documenta quali nomi esterni una funzione annidata cattura e usa nonlocal quando devi davvero riassegnare.
  • Scegli nomi descrittivi e non in conflitto. Un underscore iniziale (per esempio, _cache) segnala uso interno.
  • Tratta locals()/globals() come strumenti di diagnostica in sola lettura, non come meccanismo di configurazione.

Conclusione

Python risolve i nomi cercando negli scope Locale → Contenitore → Globale → Built-in. Capire questo ordine—e quando usare global o nonlocal—elimina errori comuni come NameError e UnboundLocalError. Tieni le variabili nello scope più piccolo possibile, evita di oscurare i built-in e usa le closure con intenzione. Con questo modello mentale, le tue funzioni si comportano in modo prevedibile e lo scope smette di essere una fonte di sorprese.

Sejal Jaiswal

Autore

Sejal Jaiswal

Ho lavorato in diversi settori e ho ricoperto vari ruoli: sviluppatore software, ricercatore di machine learning, data scientist, product manager. Ma, soprattutto, sono un programmatore a cui piace imparare e condividere conoscenza!

Argomenti

Correlato