Skip to content
CodeKruPro

Predict the output · Python

Scope, closures and lambdas

Which variable a name refers to, and what a closure captures.

Learn: Scope, closures and late binding in Python7 minute read

Scope is the part of a program where a name can be used. Python decides what a name means by a fixed set of rules, and most scope questions are built on one of them. Closures add a twist that differs from Java: they remember variables, not values. Interviewers ask about scope because the errors it causes, such as UnboundLocalError or a counter that never changes, show up in real test helpers and fixtures (code that sets up data for tests).

Rule 1: names are looked up local, enclosing, global, built-in

This order is often called LEGB. Python first looks in the current function, then in any enclosing functions, then at module level (global: the top of the file, outside every function), then in the built-in names such as len. LEGB stands for Local, Enclosing, Global, Built-in. A class body is not part of this chain: a method that uses a bare name (a name with no dot in front of it) skips the class attributes and goes on to the globals. Use self.name or the class name to reach a class attribute.

Rule 2: any assignment makes a name local for the whole function

Python decides the scope of each name before the function runs. If a function assigns to x anywhere, even on its last line, x is local everywhere in that function. Reading it before the assignment raises UnboundLocalError (an error saying that the local name has no value yet), even when a global x exists. Calling a method on a global object, such as items.append(1), is not an assignment, so it works without any declaration.

Rule 3: global and nonlocal change the rule

global x makes x in this function refer to the module-level variable, so assignments change it. nonlocal x does the same for a variable in the nearest enclosing function. This is how a closure keeps a counter that survives between calls.

Rule 4: there is no block scope

for, while, if, with and try do not create a new scope. A loop variable still exists after the loop and keeps its last value. Comprehensions (short loops written inside brackets, such as [j * j for j in range(3)]) are the exception: their loop variable stays inside the comprehension.

Rule 5: closures capture variables, not values

A closure is an inner function that uses variables from the function around it. It keeps a link to the variable and reads its current value when it is called. If the variable changes after the inner function is created, the inner function sees the change. That is why lambdas (small functions written in one line) created in a loop all return the last value of the loop variable. This is called late binding. Bind the current value with a default argument, lambda k=k: k, because default values are evaluated when the function is created.

Three lambdas written as lambda: i all read one variable i, which is 2 when they are called; with lambda i=i: i each lambda keeps its own value 0, 1 or 2.
Without a default, every lambda reads the same i. With i=i, each keeps its own value.
x = "global"

def outer():
    x = "enclosing"
    def inner():
        return x
    x = "changed"
    return inner

print(outer()())
for k in range(2):
    pass
print(k)
print([lambda: k for k in range(3)][0]())
changed
1
2

Rule 6: defaults are evaluated once

A default argument value is computed when def runs. If it reads a global, later changes to that global do not affect it. If it is a mutable object, every call shares the same object.

The next sample goes back to Rule 3. count is module-level state changed with global, and every call of make gets its own n, changed with nonlocal.

count = 0

def bump():
    global count
    count += 1

def make():
    n = 0
    def step():
        nonlocal n
        n += 10
        return n
    return step

bump()
s = make()
s()
print(count, s())
1 20

How to answer in an interview

For each name, say which scope it belongs to before you trace values. "x is assigned inside f, so it is local in all of f, and the first print raises UnboundLocalError." For the lambda question, give the output, name it late binding, and show the i=i fix. Comparing with Java helps: Java lambdas may only use locals that never change, so this trap cannot happen there.

Common mistakes

  • Expecting a function to read the global before its own assignment to the same name.
  • Using global to append to a list, where it is not needed.
  • Expecting a loop variable to disappear after the loop.
  • Expecting lambdas made in a loop to remember different values.
  • Expecting a method to find a class attribute by its bare name.

Recap

Lookup order is local, enclosing, global, built-in, and class bodies are skipped. Any assignment makes a name local for the whole function unless global or nonlocal says otherwise. Loops and ifs create no scope. Closures read variables when called, and defaults are fixed when the function is defined.

10 questions0/10 solved

Not startedNot startedIn progressIn progressSolvedSolvedPro+Pro+

The first 2 questions of each topic are free. Pro+ opens every question.

Back to the plan