Perfect.
DAY 15 is a turning point — this is where Python goes from “procedural + OOP” to powerful functional runtime behavior.

Most Python devs use decorators.
Very few understand them.
After today, you will.


🧠 DAY 15 — Closures & Decorators

(Free Variables, Cell Objects & Function Wrapping)


🔑 CORE IDEA OF DAY 15

A closure is a function that remembers variables from its enclosing scope, even after that scope is gone.

Decorators are just closures + functions-as-objects.


1️⃣ What Is a Closure? (Precise Definition)

A closure exists when:

  1. A function is defined inside another function
  2. The inner function references variables from the outer function
  3. The inner function survives after the outer function returns
def outer(x):
    def inner():
        return x
    return inner

f = outer(10)
print(f())   # 10

x still exists — even though outer() has finished.


2️⃣ Why Does This Work? (Runtime Truth)

When outer() finishes:

  • Its frame is destroyed
  • BUT variables referenced by inner are stored in a cell object

This is the key insight.


3️⃣ Free Variables & Cell Objects (CPython Level)

def outer(x):
    def inner():
        return x
    return inner

f = outer(10)

Inspect:

f.__closure__        # tuple of cell objects
f.__closure__[0].cell_contents   # 10

🧠 Cell objects keep data alive beyond frame lifetime


4️⃣ Closure Memory Model (Visual)

Image
Image
Image
Image

Conceptually:

inner function
 ├── __code__
 ├── __globals__
 └── __closure__ → cell → x = 10

5️⃣ Why Closures Are Powerful

Closures allow:

  • State without classes
  • Encapsulation
  • Function factories
  • Decorators
  • Callbacks

Example (function factory):

def power(n):
    def inner(x):
        return x ** n
    return inner

square = power(2)
cube = power(3)

Each closure has its own state.


6️⃣ The Famous Late-Binding Trap 🔥🔥🔥

funcs = []
for i in range(3):
    def f():
        return i
    funcs.append(f)

print([f() for f in funcs])

❌ Output:

[2, 2, 2]

WHY?

  • Closures capture variables, not values
  • i is looked up at call time
  • Loop ends with i = 2

7️⃣ Correcting Late Binding (Interview Favorite)

Solution 1: Default Argument Trick

funcs = []
for i in range(3):
    def f(i=i):
        return i
    funcs.append(f)

[0, 1, 2]

Why this works:

  • Default arguments evaluated immediately
  • Value captured, not name

Solution 2: Helper Function

def make_func(i):
    def f():
        return i
    return f

8️⃣ Decorators — What They REALLY Are

A decorator is:

A function that takes a function and returns a function

def decorator(func):
    def wrapper():
        print("before")
        func()
        print("after")
    return wrapper

Usage:

@decorator
def hello():
    print("hello")

Equivalent to:

hello = decorator(hello)

9️⃣ Decorator Execution Order (VERY IMPORTANT)

@decorator
def f():
    pass

Execution order:

  1. f is defined
  2. decorator(f) is executed immediately
  3. Result replaces f

⚠️ Decorators run at definition time, not call time.


🔟 Why functools.wraps Exists (CRITICAL)

Without it:

def decorator(func):
    def wrapper():
        return func()
    return wrapper

You lose:

  • __name__
  • __doc__
  • __annotations__

Correct pattern:

from functools import wraps

def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

Interviewers expect this.


11️⃣ Decorators with Arguments (Advanced)

def repeat(n):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(n):
                func(*args, **kwargs)
        return wrapper
    return decorator

Usage:

@repeat(3)
def hello():
    print("hi")

Decorator layers:

repeat(3) → decorator → wrapper

12️⃣ Closures vs Classes (Design Choice)

Closures:

  • Lightweight
  • Simple state
  • Functional style

Classes:

  • Complex state
  • Multiple methods
  • Clear structure

Senior engineers choose intentionally.


🔥 INTERVIEW TRAPS (DAY 15)

Q1

def outer():
    x = 10
    def inner():
        x += 1
        return x
    return inner

❌ Error
Why?

  • Assignment makes x local
  • Use nonlocal x

Q2

def counter():
    x = 0
    def inc():
        nonlocal x
        x += 1
        return x
    return inc

✔ Valid closure-based state


Q3

Why decorators don’t slow code too much?

✔ Wrapper is just one extra function call
✔ Cost predictable
✔ Often negligible compared to I/O


🧠 DAY 15 MENTAL CHECKLIST

When using closures/decorators:

  1. Am I capturing variable or value?
  2. Will late binding bite me?
  3. Do I need nonlocal?
  4. Did I preserve metadata (wraps)?
  5. Is closure simpler than a class here?

📝 DAY 15 ASSIGNMENT (MANDATORY)

1️⃣ Predict output (NO RUNNING):

def make_funcs():
    return [lambda: i for i in range(3)]

funcs = make_funcs()
print([f() for f in funcs])

2️⃣ Fix it in TWO ways

(Default arg + helper function)


3️⃣ Explain clearly:

  • What a cell object is
  • Why closures survive after outer returns

🔜 DAY 16 PREVIEW

DAY 16 — Lambda & Functional Programming in Python

You’ll learn:

  • When lambdas are good
  • When they are dangerous
  • map, filter, reduce internals
  • Functional vs Pythonic style tradeoffs

When ready, say 👉 “START DAY 16”