You think a function is fixed once defined. It is not.

In Python, functions are objects—and can be modified at runtime.

Today, you understand how decorators actually work under the hood.


Today’s Goal

By the end of today, you will:

  • Understand how decorators work internally
  • Learn function wrapping
  • Understand closures
  • Use decorators for real-world patterns

The Illusion

def greet():
    print("hello")

You think:

greet is just a function

Reality:

greet is an object that can be passed, modified, and replaced


Functions Are First-Class Objects

def greet():
    print("hello")

f = greet
f()

Functions can be:

  • assigned
  • passed
  • returned

What Is a Decorator?

A decorator is:

a function that takes another function and returns a new function


Basic Example

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

Applying Decorator

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

Equivalent to:

greet = decorator(greet)

Key Insight

Decorators replace your function with another function.


Closures (Critical)

def outer():
    x = 10

    def inner():
        print(x)

    return inner

Inner function remembers x.


Why Closures Matter

Decorators rely on closures to:

  • retain original function
  • maintain state

Decorator with Arguments

def decorator(func):
    def wrapper(*args, **kwargs):
        print("before")
        result = func(*args, **kwargs)
        print("after")
        return result
    return wrapper

Metadata Problem

print(greet.__name__)

Returns:

wrapper

Fix with functools.wraps

from functools import wraps


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

Parameterized Decorator

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

Example Usage

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

Real-World Use Cases

  • logging
  • authentication
  • caching
  • timing functions

Example — Timing Decorator

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print("time:", end - start)
        return result
    return wrapper

Stacking Decorators

@d1
@d2
def f():
    pass

Equivalent:

f = d1(d2(f))

Why This Matters

Decorators allow:

  • clean abstraction
  • separation of concerns
  • reusable behavior

Your Task

  • build logging decorator
  • implement retry decorator
  • stack multiple decorators

Common Mistakes

  • forgetting *args, **kwargs
  • losing function metadata
  • overusing decorators

Think Deeper

  1. how does closure store variables?
  2. how does decorator replace function?
  3. when should you avoid decorators?

Subtle Insight (CRITICAL)

Decorators modify behavior without changing original code.


Tomorrow

Concurrency & Async — how Python handles parallel work


Rule

  • decorators wrap, they don’t change original logic
  • use for cross-cutting concerns

See you in Day 10.