Nested Functions, Closures and nonlocal
- Define a function inside a function and explain what it sees from the Enclosing Scope
- Explain what a Closure is — a function that remembers variables even after the outer function has ended
- Understand why changing an outer variable causes an UnboundLocalError, and fix it with nonlocal
- Tell apart global and nonlocal
What will we learn?
- Nested Function — a function defined inside a function.
- Enclosing — the E of LEGB, up close.
- Closure — a function that “remembers” variables.
nonlocal— how to change a variable of the enclosing function.
Why do we need this?
- A private helper function: sometimes a function needs a small helper function that belongs only to it. There is no reason for it to be global and get mixed up with the rest of the code.
- Functions that remember state: in the previous lesson we saw that
make_multiplier(2)returns a function that “remembers” the number 2. Today we will understand how it works — and how to build a counter, an accumulator or a cache withoutglobal.
Explanation from the basics
A function inside a function
You can define a def inside a def. The inner function:
- Exists only inside the outer function — from outside you cannot call it.
- Sees the variables and parameters of the outer function.
Enclosing — the E of LEGB
In the lesson on Scope we learned the lookup order L → E → G → B. When an inner function uses a name, Python looks for it:
L Local → inside the inner function
E Enclosing → inside the outer function
G Global → top level of the file
B Built-in → print, len, ...Closure — a function that remembers
When an outer function returns the inner function, something interesting happens: the outer function has already ended, but the inner one still remembers its variables. This combination — a function + the variables it remembers — is called a Closure.
The outer function writes a letter (the inner function) and puts it in an envelope together with a note with some data (the variables). It sends the envelope and disappears — but whoever opens the envelope gets both the letter and the note. Every envelope has a note of its own.
nonlocal
Reading a variable from the Enclosing scope — is always allowed. Changing it (assigning it a new value) — is not possible without a declaration, exactly as with a global. The declaration is nonlocal name: “this variable belongs to the enclosing function”.
| Declaration | The variable belongs to… |
|---|---|
global x | The file level |
nonlocal x | The nearest enclosing function |
Syntax
def outer():
count = 0 # enclosing variable
def inner():
nonlocal count # assignments go to outer's count
count += 1
return count
return inner # return the function itself (no parentheses)First code example
A private helper function. week-2/receipt.py:
def format_receipt(items):
def line(name, price):
return name.ljust(10) + str(price)
lines = []
for name, price in items:
lines.append(line(name, price))
return "\n".join(lines)
print(format_receipt([("Milk", 6), ("Bread", 9)]))Output:
Milk 6
Bread 9Breaking down the code line by line
| Line | Explanation |
|---|---|
def line(name, price): | An inner function — exists only inside format_receipt. |
name.ljust(10) | Pads the name with spaces up to 10 characters, so that the prices stand in a column. |
lines.append(line(name, price)) | The outer function uses its helper function. |
"\n".join(lines) | Joins all the lines into one text, with a line break between them. |
Another example
A Closure — a function that remembers. week-2/greeter.py:
def make_greeter(greeting):
def greet(name):
return greeting + ", " + name
return greet
hello = make_greeter("Hello")
shalom = make_greeter("Shalom")
print(hello("Dana"))
print(shalom("Avi"))Output:
Hello, Dana
Shalom, Avimake_greeter already ended on the line hello = ..., but hello still remembers that greeting is "Hello". Every function that was created has its own copy of the variable — shalom remembers "Shalom".
A counter with nonlocal. week-2/counter.py:
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter = make_counter()
print(counter(), counter(), counter())
other = make_counter()
print(other())Output:
1 2 3
1Every counter remembers its own count: other starts from zero, regardless of counter. This is private state — without a global variable that all the code can change.
Common mistake
The mistake: changing a variable of the enclosing function without nonlocal.
def make_counter():
count = 0
def increment():
count += 1
return count
return increment
counter = make_counter()
counter()UnboundLocalError: cannot access local variable 'count' where it is not associated with a valueWhy? Because of the assignment count += 1, Python decides that count is a local variable of increment — and then tries to read a local that has no value yet. This is exactly the same error we saw with globals in the lesson on Scope.
The fix: nonlocal count at the start of increment.
Practice
The exercises use only what was taught in this lesson and the lessons before it. Try on your own first, and only then open the solution.
Exercise 1: An inner helper function
Write a function format_receipt(items) that receives a list of pairs (name, price) and returns the text of a receipt. Inside it, define an inner function line(name, price) that pads the name to 10 characters (ljust).
print(format_receipt([("Milk", 6), ("Bread", 9)]))Expected output:
Milk 6
Bread 9Show solution
def format_receipt(items):
def line(name, price):
return name.ljust(10) + str(price)
lines = []
for name, price in items:
lines.append(line(name, price))
return "\n".join(lines)
print(format_receipt([("Milk", 6), ("Bread", 9)]))Milk 6
Bread 9line exists only inside format_receipt — it is a private helper function, and does not get mixed up with the rest of the code.
Exercise 2: Closure
Write make_greeter(greeting) that returns a function which receives a name and returns a greeting.
hello = make_greeter("Hello")
shalom = make_greeter("Shalom")
print(hello("Dana"))
print(shalom("Avi"))Expected output:
Hello, Dana
Shalom, AviShow solution
def make_greeter(greeting):
def greet(name):
return greeting + ", " + name
return greet
hello = make_greeter("Hello")
shalom = make_greeter("Shalom")
print(hello("Dana"))
print(shalom("Avi"))Hello, Dana
Shalom, Avimake_greeter has already ended, but every function it returned remembers its own greeting. That is a Closure.
Exercise 3: Predict the output
def outer():
message = "outer"
def show():
print("show:", message)
def change():
nonlocal message
message = "changed"
show()
change()
show()
outer()Show solution
def outer():
message = "outer"
def show():
print("show:", message)
def change():
nonlocal message
message = "changed"
show()
change()
show()
outer()show: outer
show: changedchange uses nonlocal, so the assignment changes the message of outer — it does not create a new local variable.
Exercise 4: A counter with a step
Write make_counter(step) — every call to the counter increases it by step and returns the new value. Create two counters and check that they do not affect each other.
by_two = make_counter(2)
by_ten = make_counter(10)
print(by_two(), by_two())
print(by_ten())
print(by_two())Expected output:
2 4
10
6Show solution
def make_counter(step):
count = 0
def increment():
nonlocal count
count += step
return count
return increment
by_two = make_counter(2)
by_ten = make_counter(10)
print(by_two(), by_two())
print(by_ten())
print(by_two())2 4
10
6stepis only read insideincrement, so it needs no declaration.countchanges, so it needsnonlocal count.
Exercise 5: Without nonlocal
def make_counter():
count = 0
def increment():
count += 1
return count
return increment
counter = make_counter()
counter()- Which error will you get, and why?
- What is the difference between
globalandnonlocal?
Show solution
UnboundLocalError. Because of the assignmentcount += 1, Python decides thatcountis a local variable ofincrement— and it has no value when you try to increase it. The fix:nonlocal countat the start ofincrement.global— the variable belongs to the file level.nonlocal— the variable belongs to the nearest enclosing function. A Closure withnonlocalgives every counter private state, without a global variable that all the code can change.
Summary
- Nested Function — a function inside a function. It exists only inside, and sees the variables of the enclosing one (Enclosing).
- Closure — an inner function that was returned, and remembers the variables of the function that created it. Each one has its own copy.
- Reading a variable from the Enclosing scope — is allowed. Changing it — only with
nonlocal, otherwiseUnboundLocalError. global— a variable at the file level.nonlocal— a variable in the enclosing function.- A Closure gives private state without a global variable. And still — when possible, a parameter in and
returnout is better.