Scope and the LEGB Rule

Python resolves a name by looking in four places in a fixed order: local, enclosing, global, built-in. Assignment anywhere in a function makes the name local for the whole function.

The four scopes

B  Built-in     names Python always provides: print, len, str, ...
G  Global       names at the top level of the module
E  Enclosing    names in an outer function, for a nested function
L  Local        names inside the current function

Lookup order:  L -> E -> G -> B      (the first match wins)
value = "global"


def outer():
    value = "enclosing"

    def inner():
        value = "local"
        print(value)          # local

    inner()
    print(value)              # enclosing


outer()
print(value)                  # global

Three separate variables happen to share a name. Each print finds the nearest one.

Reading is easy, writing is not

count = 10


def show():
    print(count)        # 10 - reading a global works without ceremony


show()
count = 10


def increment():
    count = count + 1       # UnboundLocalError
    print(count)


# increment()

This is the single most confusing scope error in Python, and the reason is worth stating precisely:

If a name is assigned anywhere in a function, Python treats it as local for the whole function, including lines before the assignment. count = count + 1 therefore tries to read a local count that has not been assigned yet.

global

count = 10


def increment():
    global count
    count = count + 1


increment()
print(count)          # 11

global tells Python that assignments to this name in this function refer to the module level variable. It works, and it is almost always the wrong design. Prefer returning a value:

count = 10


def increment(current):
    return current + 1


count = increment(count)
print(count)          # 11

The second version can be tested, called twice safely, and reasoned about without reading the rest of the file.

nonlocal

def counter():
    count = 0

    def increment():
        nonlocal count        # the count in the ENCLOSING function
        count += 1
        return count

    return increment


tick = counter()
print(tick(), tick(), tick())     # 1 2 3

nonlocal targets the nearest enclosing function scope, never the global one. Without it, count += 1 raises UnboundLocalError for exactly the reason above. This pattern is a closure, covered fully in the advanced functions note.

globalnonlocal
TargetsModule levelNearest enclosing function
Name must already existNoYes
Typical useRarely justifiedClosures and counters

Mutation does not need a declaration

items = []
settings = {"a": 1}


def add():
    items.append(1)          # MUTATING - no global needed
    settings["b"] = 2


def replace():
    global items
    items = [9]              # REBINDING - global is required


add()
print(items, settings)       # [1] {'a': 1, 'b': 2}

The rule is about binding a name, not about changing an object. items.append(...) never rebinds items, so no declaration is needed. That is also why mutable global state is easy to change by accident.

The built-in scope

print(len("abc"))            # len comes from the built-in scope

len = 5                      # a global named len now shadows it
# print(len("abc"))          # TypeError: 'int' object is not callable

del len                      # remove the shadow, the built-in is visible again
print(len("abc"))            # 3

Shadowing list, dict, str, id, type, sum, max, min, input and next is easy to do and produces errors that look impossible. If a built in stops working, search the file for an assignment to its name.

Scopes that do not exist

if True:
    inside_if = "visible"

for i in range(3):
    pass

while False:
    pass

print(inside_if)      # visible - if does not create a scope
print(i)              # 2 - the loop variable survives

Only functions, classes, modules and comprehensions create scopes. Blocks do not. A name assigned inside an if or a for is visible afterwards, which is convenient and occasionally surprising.

n = "outer"
squares = [n * n for n in range(3)]
print(n)              # outer - a comprehension DOES have its own scope

Inspecting the scopes

value = "global value"


def demo():
    local_value = "local value"
    print(sorted(locals()))          # ['local_value']
    print("value" in globals())      # True


demo()

A worked example

TAX_RATE = 0.18          # a module level constant: acceptable global state


def line_total(price, quantity):
    subtotal = price * quantity          # local
    return subtotal * (1 + TAX_RATE)     # reads the global constant


def invoice_total(lines):
    total = 0                             # local to this function
    for price, quantity in lines:
        total += line_total(price, quantity)
    return total


print(round(invoice_total([(100, 2), (50, 3)]), 2))

Constants read from the global scope are fine. Mutable state written from several functions is what causes trouble.

Common mistakes

  • Assigning to a global inside a function without declaring it, then meeting UnboundLocalError.
  • Using global as the first solution instead of returning a value.
  • Using global where nonlocal was needed inside a nested function.
  • Shadowing a built in name.
  • Expecting if and for blocks to create scopes.
  • Relying on the loop variable after the loop without noticing it is still bound.

Best practices

  • Pass values in as parameters and hand results back with return.
  • Keep globals to constants, written in capitals.
  • Use nonlocal only inside genuine closures.
  • Never name a variable after a built in.
  • Keep functions short enough that every local name is visible at once.

Practice

  1. Explain the exact reason count = count + 1 raises UnboundLocalError when count is global.
  2. Write a counter using nonlocal, then rewrite it without any declaration at all.
  3. Demonstrate a case where a function changes a global list without using global, and explain why.
  4. Shadow list deliberately, observe the failure, and recover in the same session.
  5. Predict what n holds after a for loop and after a comprehension over the same range.

Conclusion

Names resolve local, enclosing, global, built-in. Assignment anywhere in a function makes a name local everywhere in that function - that one rule explains every scope error you will meet. Prefer parameters and return values to global.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →
Python

Lambda Functions

A lambda is a small anonymous function written as a single expression. It exists for the places where naming a function would add nothing.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.