ASAmol Shukla
Projects
Courses
Prompts
Skills
Contact
Resume
Course Outline
Syllabus Overview

Complete Python Course: From Zero to Professional

Courses/Complete Python Course: From Zero to Professional/Lesson 14: Scope, Closures & Decorators
55 mins lesson duration•10 mins read

Lesson 14: Scope, Closures & Decorators

LEGB scoping rules, closures that remember, and decorators that wrap functions with extra behavior.

Scope — Where Names Live

Python resolves names with the LEGB rule. When code refers to a name, Python searches in this order:

  1. Local — inside the current function
  2. Enclosing — in outer (nested) functions
  3. Global — at module top level
  4. Built-in — Python's built-in names like print, len, sum

The first match wins. This is why you can have a local variable named len shadowing the built-in (don't do that).

Mental model: scopes are nested rooms. Python looks in your room first (local), then the room you're inside (enclosing), then the house (global), then the city (built-ins). First room with the name wins.

What You'll Learn in This Lesson

  • Apply the LEGB scoping rules
  • Modify outer variables with global and nonlocal
  • Build closures — functions that remember
  • Write decorators that wrap functions with extra behavior

global and nonlocal

  • global lets a function modify a module-level variable.
  • nonlocal lets a nested function modify an enclosing function's variable.
count = 0

def increment_global():
    global count
    count += 1

increment_global()
print(count)   # 1

Tip: in modern code, global is rare — passing values in/out via parameters and returns is cleaner. But nonlocal is essential for closures (next).

Why do you even need global? Without it, writing count += 1 inside a function would create a new local variable called count (and crash with UnboundLocalError, since you're reading it before assignment). global tells Python "no, I mean the module-level one".


Closures — Functions That Remember

A nested function that references variables from its enclosing scope is a closure. It "remembers" those values even after the outer function returns:

def make_counter():
    count = 0
    def increment():
        nonlocal count      # modify the enclosing variable
        count += 1
        return count
    return increment

counter = make_counter()
print(counter())   # 1
print(counter())   # 2
print(counter())   # 3

The inner function increment carries a "backpack" containing count — it survives because increment still references it. This powers counters, factories, and much of functional programming in Python.

Mental model: a closure is a function with its own luggage — wherever it goes, its remembered variables come along.

A second closure example — factory functions:

def make_multiplier(n):
    def multiply(x):
        return x * n
    return multiply

times_3 = make_multiplier(3)
times_5 = make_multiplier(5)
print(times_3(10))   # 30
print(times_5(10))   # 50

Each closure (times_3, times_5) remembers its own n — independent backpacks.


Decorators — Wrap & Extend

A decorator is a function that takes another function and returns a new one with extra behavior:

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.6f}s")
        return result
    return wrapper

@timer
def slow_task():
    return sum(range(100000))

The @timer syntax is just sugar for:

slow_task = timer(slow_task)

Think of a decorator as armor around a sword: the sword (original function) still works, but now it's protected (or timed, or logged).

Mental model: the decorator is a wrapping station. Your function goes in, gets wrapped in extra behavior, and the wrapped version comes out under the same name.

Classic Decorator Uses

Use case What it adds
Timing Measures execution time
Logging Prints when a function is called
Access control Checks permissions before running
Retry logic Retries on failure
Caching Stores results of expensive calls

The @wraps Gotcha

Without care, a decorator hides the original function's name and docstring:

from functools import wraps

def timer(func):
    @wraps(func)              # copies __name__, __doc__, etc.
    def wrapper(*args, **kwargs):
        ...
    return wrapper

Always use @wraps(func) in real decorators — otherwise help() and debugging tools show the wrapper instead of your function.

Why it matters: without @wraps, slow_task.__name__ becomes "wrapper" instead of "slow_task" — breaking debuggers, documentation, and anything that inspects function metadata. @wraps copies the identity over.


Real-World Decorators You Already Use

If you have used Flask or FastAPI, you have seen decorators in action:

from flask import Flask
app = Flask(__name__)

@app.route("/")          # a decorator!
def home():
    return "Hello"

The machinery you learned here — @name = f = name(f) — is exactly what powers these web frameworks.


Common Mistakes to Avoid

  • Mistake: Using global when you meant nonlocal (or vice versa) — Fix: global = module level; nonlocal = enclosing function.
  • Mistake: A decorator that forgets to return func(*args, **kwargs) — Fix: always return the wrapped result.
  • Mistake: Missing @wraps and losing the function's identity — Fix: decorate the wrapper.
  • Mistake: Forgetting nonlocal in a closure and getting UnboundLocalError — Fix: declare nonlocal count before modifying it.
  • Mistake: Applying a decorator without the @ and forgetting to reassign — Fix: f = decorator(f) or use the @ sugar.

Professional Tips & Tricks

  • Use nonlocal (not global) to modify an enclosing function's variable.
  • Decorate with @functools.wraps(func) so the wrapped function keeps its name and docstring.
  • Decorators are the foundation of Flask/FastAPI route decorators — this is the same machinery.
  • Keep decorators generic with *args, **kwargs so they work on any function.
  • For parameterized decorators, add one more wrapping layer (def repeat(times): def deco(func): ...).

Key Takeaways

  • LEGB: Local → Enclosing → Global → Built-in.
  • global reaches module scope; nonlocal reaches enclosing function scope.
  • Closures remember enclosing variables — perfect for counters and factories.
  • Decorators wrap functions with extra behavior; @name is sugar for f = name(f).
  • Always use @wraps to preserve function metadata.

Next up: Modules, packages & imports — organizing code into files.

Interactive Lesson Code Snippet
# Closure: counter that remembers
def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

counter = make_counter()
print("Call 1:", counter())
print("Call 2:", counter())
print("Call 3:", counter())

# Decorator: time any function
import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.6f}s")
        return result
    return wrapper

@timer
def slow_task():
    total = sum(range(100000))
    return total

print("Sum:", slow_task())
Language: python

Lesson Code (Python)

# Closure: counter that remembers
def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

counter = make_counter()
print("Call 1:", counter())
print("Call 2:", counter())
print("Call 3:", counter())

# Decorator: time any function
import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.6f}s")
        return result
    return wrapper

@timer
def slow_task():
    total = sum(range(100000))
    return total

print("Sum:", slow_task())

Console Output

Call 1: 1
Call 2: 2
Call 3: 3
slow_task took 0.004812s
Sum: 4999950000

Code Visualization Tips

  • 🧠Draw scopes as nested boxes and imagine a telescope pointing outward from the innermost box.
  • 🧠For closures, picture the inner function carrying a backpack with the outer variables inside.
  • 🧠For decorators, picture wrapping a gift: the wrapper is the paper, the original function is the gift inside.

Professional Tips & Tricks

  • ⚡Use nonlocal (not global) to modify an enclosing function's variable.
  • ⚡Decorate with @functools.wraps(func) so the wrapped function keeps its name and docstring.
  • ⚡Decorators are the foundation of Flask/FastAPI route decorators — this is the same machinery.

Python Code Judge & Practice Arena

LeetCode Style

Run real Python 3.12 WebAssembly code directly in your browser against automated test suites.

Solved:0 / 20
0 / 370 XP
Challenges:
Problem 1 of 20

Problem 1: Stateful Closure Counter 1

Easy+10 XP
Write a function `create_accumulator_1(initial=0)` that returns a closure adding values to the running total.
Sample Test Cases:
Input: (lambda acc: [acc(5), acc(10), acc(2)])(create_accumulator_1(0))
Expected: [5, 15, 17]
Input: (lambda acc: acc(20))(create_accumulator_1(100))
Expected: 120
main.pyPython 3.12 (WASM)
1
2
3
4
5
6
7
8
9
10
11
12
Press Run Code to test or Submit to verify test cases

Test Your Knowledge

Instant feedback

Quick Check: Lesson 14: Scopes (LEGB), Closures & Decorators

1 / 20
What does the LEGB rule stand for in Python variable resolution?

Up next · Continue learning

Modules, Packages & Imports

Organize code into files, import between them, use the __name__ guard, and install packages with pip.

8 mins read40 mins
Start next lesson
Previous: Lambda, *args & **kwargsNext: Modules, Packages & Imports
Made withbyAmol Shukla·amolshukla.online
ASAmol Shukla

AI Developer, Trainer & Agentic AI Expert building practical learning systems and real-world AI applications.

Explore

  • Projects
  • Courses
  • Prompts
  • Skills
  • Contact
  • Experience
  • Blogs

Connect

  • Resume
  • Contact
© 2026 Amol Shukla·Created withbyamolshukla.online
Back to top