CodeOath
← All posts
Python70 min total · 18 parts

Python Fundamentals for Interviews: Data Structures, Comprehensions, and Gotchas

Contents — Part 7 of 18: Functions, Scope, and Closures
Part 7 of 18 · ~2 min

Functions, Scope, and Closures

Python resolves a variable name using the LEGB rule: Local, Enclosing, Global, Built-in — in that order, the first match wins.

x = "global"

def outer():
    x = "enclosing"
    def inner():
        x = "local"
        print(x)  # "local" — found in Local scope first
    inner()
    print(x)      # "enclosing" — inner's assignment didn't touch outer's x

Closures

A closure is an inner function that remembers variables from its enclosing scope, even after the outer function has finished running:

def make_multiplier(factor):
    def multiply(n):
        return n * factor  # "factor" is captured from make_multiplier's scope
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
double(5)  # 10 — remembers factor=2 from when it was created
triple(5)  # 15 — a completely separate closure, remembers factor=3

The late-binding closure bug in loops

This is one of the most common "predict the output" interview questions in Python:

funcs = []
for i in range(3):
    funcs.append(lambda: i)

[f() for f in funcs]  # [2, 2, 2] — NOT [0, 1, 2]!

Each lambda closes over the variable i, not its value at creation time — by the time any lambda is actually called, the loop has finished and i is 2 for all three. The fix is to force evaluation at each iteration via a default argument (default values ARE evaluated immediately, at function-definition time — see the mutable default section below):

funcs = []
for i in range(3):
    funcs.append(lambda i=i: i)  # i=i captures the CURRENT value as a default

[f() for f in funcs]  # [0, 1, 2] — correct

nonlocal and global

def counter():
    count = 0
    def increment():
        nonlocal count  # without this, "count += 1" would raise UnboundLocalError
        count += 1
        return count
    return increment

c = counter()
c()  # 1
c()  # 2

Without nonlocal, assigning to count inside increment would make Python treat count as a brand-new local variable in increment's own scope (because Python decides a variable's scope for the entire function body at compile time, based on whether it's assigned anywhere in that function) — and reading it before that local assignment raises UnboundLocalError, not a fallback to the outer count.