Ga naar hoofdinhoud

Python-variabelenscope en de LEGB-regel uitgelegd

Leer waar variabelenscopes om draaien en raak vertrouwd met de 'LEGB'-regel. Je ziet ook scenario’s waarin de keywords global en nonlocal in actie komen.
· 8 min lezen

Verken met AI

Meteen na een paar regels code "name is not defined" of "local variable referenced before assignment" zien is frustrerend. De code oogt prima, maar Python is het er niet mee eens. De oorzaak is bijna altijd scope: waar een naam zichtbaar is en naar welk object hij op dat moment verwijst.

Zodra je de naamresolutie van Python begrijpt—vaak samengevat als de LEGB-regel—worden deze fouten voorspelbaar en eenvoudig te verhelpen. In deze tutorial behandel ik stap voor stap variabelen en scopes in Python.

Wat is een variabele?

Een variabele is een naam die aan een object is gebonden. In Python maak je zo'n binding met de toewijzingsoperator =. Namen hebben geen type; ze kunnen naar elk object verwijzen (string, int, list, function, enz.).

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

Volg deze regels en conventies bij het benoemen van variabelen om syntaxfouten en subtiele bugs te voorkomen.

  • Gebruik alleen letters, cijfers en underscores; namen mogen niet met een cijfer beginnen.
  • Gebruik geen keywords zoals True, for of class als naam.
  • Vermijd het overschaduwen van built-ins zoals list, dict, sum of max.
  • Geef de voorkeur aan snake_case voor leesbaarheid (PEP 8).

Deze voorbeelden laten zien wat je niet moet doen en welke fouten Python geeft:

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

Hoe Python-scope werkt (LEGB)

Scope is het codegebied waar een naam zichtbaar is. Wanneer je een naam gebruikt, zoekt Python deze in deze volgorde op (LEGB):

  • Local: de scope van de huidige functie.
  • Enclosing: eventuele omvattende functiescopes (bij geneste functies).
  • Global: het topniveau van de module (modulenamespace).
  • Built-in: namen die door Python in builtins zijn gedefinieerd (bijvoorbeeld len, print).

Namen leven in namespaces (denk aan dictionaries die namen aan objecten koppelen). Scope gaat over welke namespaces Python raadpleegt om een naam op een bepaald punt in de code te resolven.

Lokale scope

Namen die binnen een functie worden toegewezen, zijn lokaal voor die functie, tenzij anders gedeclareerd. Ze zijn niet zichtbaar buiten de functie.

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

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

Python bepaalt scope tijdens het compileren. Als een functie ergens in haar body aan een naam toewijst, worden alle gebruiken van die naam in de functie als lokaal beschouwd—wat leidt tot een veelvoorkomende UnboundLocalError wanneer je leest voordat je toewijst:

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

Om de module-niveau naam in de functie te gebruiken, vermijd je óf de toewijzing, óf markeer je hem als global (hieronder behandeld).

Enclosing scope (closures)

Geneste functies kunnen namen zien uit hun direct omvattende functie. Om zo'n naam opnieuw te binden (niet alleen lezen), declareer je hem als 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

Zonder nonlocal zou toewijzen aan count binnen increment() een nieuwe lokale naam creëren en de buitenste count ongemoeid laten.

Globale scope

Namen die op het topniveau van een module worden toegewezen, leven in de globale namespace van de module. Elke functie kan ze lezen. Om binnen een functie aan een naam op module-niveau toe te wijzen, declareer je hem als 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

Gebruik global spaarzaam. Geef de voorkeur aan waarden doorgeven als parameters en resultaten retourneren om code testbaar en voorspelbaar te houden.

Built-in scope (en een opmerking over keywords)

De built-in scope bevat namen zoals len, print en Exception. Vermijd het overschaduwen ervan, anders verlies je in die scope de toegang tot de built-in.

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

Keywords (zoals if, for, def) maken deel uit van de Python-syntaxis, niet van de built-in namespace, en kunnen nooit als identifiers worden gebruikt.

Blokken, lussen en comprehensions

De blokstatements en comprehensions van Python hebben specifieke scope-eigenschappen die ontwikkelaars uit andere talen vaak verrassen.

Er is geen block scope voor if/for/while/with

Toewijzingen binnen deze blokken beïnvloeden de omvattende scope (functie of module). Lusvariabelen blijven ook gedefinieerd nadat de lus is afgelopen.

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

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

Comprehensions isoleren hun iteratievariabele

List-, dict- en set-comprehensions hebben hun eigen lokale scope voor lusvariabelen. De iteratievariabele lekt niet naar de omringende scope.

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

Python 3.12 (PEP 709) inline-de comprehensions voor snelheid, met behoud van deze isolatie; je krijgt nog steeds duidelijke, niet-lekkende lusvariabelen met snellere uitvoering.

Het keyword global gebruiken

Declareer een naam als global binnen een functie wanneer je een variabele op module-niveau opnieuw moet binden. Plaats de declaratie bovenaan de functie zodat duidelijk is welke variabele je wijzigt.

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

Het keyword nonlocal gebruiken

Gebruik nonlocal om een naam uit de dichtstbijzijnde omvattende functiescope opnieuw te binden. Dit is gebruikelijk in closures die state bijhouden.

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

Het gebruik van nonlocal voor een naam die niet in een omvattende functie is gedefinieerd, is een SyntaxError. Als de naam global is, gebruik dan global.

locals() en globals() in 2025

Deze functies zijn handig voor inspectie en debuggen, niet om variabelen bij te werken. Vanaf Python 3.13 (PEP 667) geeft elke aanroep van locals() in een functie een onafhankelijke snapshot terug. Het bewerken van die snapshot verandert de echte locals niet. Ik heb dit bevestigd door het volgende fragment op Python 3.13 uit te voeren:

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')

Gebruik globals() op een vergelijkbare manier voor de modulenamespace. Geef bij applicatiecode de voorkeur aan expliciete parameters en returnwaarden boven dynamische lookups.

Veelvoorkomende valkuilen en hoe je ze oplost

Deze fouten verklaren het merendeel van de scope-gerelateerde issues die ik in de praktijk zie.

  • UnboundLocalError door toewijzing in een functie: Python beschouwt een naam als lokaal als eraan ergens in de functie wordt toegewezen. Verplaats het lezen na de toewijzing, hernoem, of voeg waar passend global/nonlocal toe.
  • Blokscope verwachten: Namen die in if/for/while/with worden toegewezen, blijven bestaan in de omvattende scope. Gebruik kleinere helperfuncties om de scope te beperken.
  • Built-ins overschaduwen: Vermijd namen als list, dict, sum, id of min. Kies beschrijvende namen zoals customer_list of min_allowed.
  • nonlocal vergeten in closures: Als je een variabele uit een buitenstaande functie wilt bijwerken, declareer hem dan als nonlocal. Anders maak je een nieuwe lokale aan en verandert de buitenste waarde niet.
  • Keywords verwarren met built-ins: Keywords (syntaxis) kunnen nooit als namen worden gebruikt. Built-ins kunnen worden overschaduwd maar dat is onwenselijk.

Best practices om scope eenvoudig te houden

Deze gewoontes maken code makkelijker leesbaar, testbaar en te debuggen.

  • Geef de voorkeur aan kleine, gefocuste functies. Definieer variabelen in de kleinst mogelijke scope die werkt.
  • Geef data door en terug via parameters en returnwaarden. Minimaliseer state op module-niveau.
  • Gebruik closures bewust. Documenteer welke buitenliggende naamen een geneste functie vastlegt, en gebruik nonlocal wanneer je echt opnieuw moet binden.
  • Kies beschrijvende, niet-conflicterende namen. Een leading underscore (bijvoorbeeld _cache) duidt intern gebruik aan.
  • Beschouw locals()/globals() als read-only diagnostiek, niet als configuratiemechanisme.

Conclusie

Python resolveert namen door te zoeken in Local → Enclosing → Global → Built-in scopes. Het begrijpen van die volgorde—en wanneer je global of nonlocal gebruikt—elimineeert veelvoorkomende fouten zoals NameError en UnboundLocalError. Houd variabelen in de kleinst haalbare scope, vermijd het overschaduwen van built-ins en gebruik closures doelbewust. Met dat mentale model gedragen je functies zich voorspelbaar en houdt scope op een bron van verrassingen te zijn.

Sejal Jaiswal

Auteur

Sejal Jaiswal

Ik heb in verschillende sectoren gewerkt en meerdere petten gedragen: softwareontwikkelaar, machine learning-onderzoeker, data scientist, productmanager. Maar in de kern ben ik een programmeur die graag leert en kennis deelt!

Onderwerpen