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.
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
globalto 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
| Status | Question | Difficulty | Asked in |
|---|---|---|---|
| Not started | A local variable with the same name as a global | Easy | Asked in: TCS |
| Not started | Changing a global with the global keyword | Easy | Asked in: TCS |
| Pro+ | Loop variables after the loop | Easy-Medium | |
| Pro+ | Reading a variable before assigning it in a function | Medium | |
| Pro+ | A counter built with a closure and nonlocal | Medium | |
| Pro+ | Changing a global list without the global keyword | Medium | |
| Pro+ | A default argument taken from a variable | Medium | |
| Pro+ | A closure reads the variable, not a copy | Medium-Hard | |
| Pro+ | Lambdas created in a loop | Medium-Hard | |
| Pro+ | A method that reads a name defined in the class body | Hard |
The first 2 questions of each topic are free. Pro+ opens every question.