Academy
ProgrammingPython IntermediateScope & Closures

Scope & Closures

The Question Python Always Asks

Every time Python encounters a name — price, name, total — it asks: which variable does this refer to? The answer depends on scope — the region of a program where a name is visible and accessible.

You've already seen a glimpse of this: a variable created inside a function isn't accessible outside it. That's scope at work. This lesson makes the rules explicit.

The LEGB Rule

Python looks up names in a fixed order, moving outward through four layers:

  1. L — Local: Inside the current function
  2. E — Enclosing: Inside any outer functions that contain this one
  3. G — Global: At the top level of the module
  4. B — Built-in: Python's built-in names (len, print, range, etc.)

Python searches each layer in order and stops at the first match. If nothing is found anywhere, you get a NameError.

Three functions, three scopes, three different x values — none interfering with the others. Each print() finds the nearest x and stops looking.

Reading a Global Variable

A function can read a global variable without any special syntax:

apply_discount reads discount_rate from the global scope. Python finds it at the G layer and uses it.

Modifying a Global Variable

Reading a global is fine. Modifying one from inside a function requires the global keyword — otherwise Python assumes you're creating a new local variable.

Without global count, the line count += 1 would crash — Python would try to read a local count that hasn't been assigned yet.

In practice, modifying globals from functions is considered poor style — it makes programs hard to reason about, because any function anywhere could change shared state. It's worth knowing global exists, but reaching for it regularly is a sign the code needs restructuring.

Closures — Functions That Remember

Now for something genuinely powerful. What happens when an inner function references a variable from its enclosing function — and that inner function outlives the outer one?

make_multiplier(2) creates and returns the inner multiply function. At this point, make_multiplier has finished — its local scope is gone. And yet double(10) still produces 20. How?

Because multiply closed over the variable factor. It captured a reference to factor from the enclosing scope and carries that reference with it — even after the enclosing function is long gone.

This is a closure: a function bundled together with the variables from the scope in which it was defined.

A Practical Closure

Each call to make_counter() creates an independent counter with its own count. counter and page_counter have completely separate states — they don't share anything.

Notice count is a list containing one number, not a plain integer. This is a subtle necessity: closures can read and mutate the contents of an enclosing variable, but reassigning the variable itself (like count = count + 1) would create a new local variable instead. Wrapping the value in a list sidesteps this. In Python 3, you can use the nonlocal keyword instead:

nonlocal count tells Python: don't create a new local variable — use the count from the enclosing scope. Cleaner than the list workaround.

Why Closures Matter

Closures let you create functions with private, persistent state — without needing classes or global variables. Each closure carries its own memory, invisible to the outside world.

You'll encounter closures constantly in Python — in decorators, in callbacks, in any situation where behaviour needs to be configured at creation time rather than at call time. Understanding them removes a large category of "why does this work?" confusion from more advanced code.

What You Have Learned

Scope rules determine which variable Python uses when a name appears — closures let functions carry those variables across time.

The key ideas:

  • Python resolves names using LEGB: Local → Enclosing → Global → Built-in
  • Functions can read global variables freely; modifying them requires global
  • A closure is an inner function that captures variables from its enclosing scope
  • The enclosed variables persist as long as the closure exists
  • nonlocal allows an inner function to reassign an enclosing variable (not just read it)
  • Closures enable functions with private, independent state — no classes required

In the next lesson, you'll learn how to handle errors gracefully — writing programs that can fail without crashing.