Nested Functions, Closures and nonlocal

Lesson goals
  • 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 without global.

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.

Analogy: a letter in an envelope

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”.

DeclarationThe variable belongs to…
global xThe file level
nonlocal xThe 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     9

Breaking down the code line by line

LineExplanation
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, Avi

make_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
1

Every 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 value

Why? 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     9
Show 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     9

line 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, Avi
Show 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, Avi

make_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: changed

change 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
6
Show 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
6
  • step is only read inside increment, so it needs no declaration.
  • count changes, so it needs nonlocal count.

Exercise 5: Without nonlocal

def make_counter():
    count = 0
    def increment():
        count += 1
        return count
    return increment

counter = make_counter()
counter()
  1. Which error will you get, and why?
  2. What is the difference between global and nonlocal?
Show solution
  1. UnboundLocalError. Because of the assignment count += 1, Python decides that count is a local variable of increment — and it has no value when you try to increase it. The fix: nonlocal count at the start of increment.
  2. global — the variable belongs to the file level. nonlocal — the variable belongs to the nearest enclosing function. A Closure with nonlocal gives 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, otherwise UnboundLocalError.
  • 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 return out is better.

Official documentation