PyComplete Python Course
Advanced 50 min

Module 12 β€” Closures & Decorators

Functions that remember, and functions that wrap other functions.

Prerequisite: Module 11 β€” Functional Programming

After this lesson, you will be able to

  • Explain what a closure is and why an inner function can 'remember' outer variables
  • Write a function that returns another function
  • Write and apply a decorator using the @ syntax

Concept & Syntax

These two ideas trip up more beginners than almost anything else in this course, so take it slowly.

A closure happens when an inner function uses a variable from its enclosing (outer) function, and keeps access to it even after the outer function has finished running. It's how a function can carry its own private memory around with it.

A decorator is a function that takes another function as input, and returns a new function that usually calls the original β€” plus does something extra before or after. The @decorator syntax above a function definition is just a shortcut for my_function = decorator(my_function).

Syntax
def decorator(func):
    def wrapper(*args, **kwargs):
        # do something before
        result = func(*args, **kwargs)
        # do something after
        return result
    return wrapper
 
@decorator
def my_function():
    ...

Examples

A function returning a function Very Easy

A function returning a function
def make_multiplier(factor):
    def multiply(number):
        return number * factor
    return multiply
 
times3 = make_multiplier(3)
print(times3(10))
Output30

make_multiplier(3) returns a brand-new function, multiply, that has 'remembered' factor = 3 β€” that's the closure.

A closure keeping private state Intermediate

A closure keeping private state
def make_counter():
    count = 0
    def counter():
        nonlocal count
        count += 1
        return count
    return counter
 
count_up = make_counter()
print(count_up())
print(count_up())
print(count_up())
Output1 2 3

count lives inside make_counter's scope, but counter() keeps a private, persistent reference to it via nonlocal β€” each call remembers the last.

Your first decorator Real World

Your first decorator
def announce(func):
    def wrapper():
        print("Starting...")
        func()
        print("Done!")
    return wrapper
 
@announce
def say_hi():
    print("Hi!")
 
say_hi()
OutputStarting... Hi! Done!

@announce is shorthand for say_hi = announce(say_hi). Calling say_hi() now actually runs wrapper(), which adds behavior before and after the original function.

A decorator that accepts arguments Challenge

A decorator that accepts arguments
def announce(func):
    def wrapper(*args, **kwargs):
        print("Starting...")
        result = func(*args, **kwargs)
        print("Done!")
        return result
    return wrapper
 
@announce
def add(a, b):
    return a + b
 
print(add(2, 3))
OutputStarting... Done! 5

Using *args and **kwargs in wrapper lets the decorator work on any function, regardless of what arguments it takes.

Visual Explanation

Original functionPassed into a decoratorDecorator returns a wrapper function@decorator replaces the name with wrapperCalling it now runs the wrapper's extra behavior
A decorator wraps a function to add behavior without changing the original function's code.
πŸ’‘ Why does this work?

Read the concept explanation above again slowly, line by line β€” every keyword there maps directly onto a line of code in the examples. If it still doesn't click, re-run the traced example (if this lesson has one) one step at a time.

πŸ” What happens internally? (advanced)

Under the hood, Python compiles your source into bytecode and runs it on a virtual machine (the CPython interpreter). Variables are names in a namespace dictionary pointing at objects in memory β€” which is exactly what the memory panel in the tracer above is showing you.

Common Mistakes

βœ— Common mistake 1

Forgetting to return the result of func() inside a wrapper silently discards the original return value.

βœ— Avoid this
This raises prints None instead of 5
def announce(func):
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)  # result is thrown away!
    return wrapper
 
@announce
def add(a, b):
    return a + b
 
print(add(2, 3))
βœ“ Better approach
Fixed
def announce(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper
βœ“ Best practices
  • Always accept *args and **kwargs in a wrapper so the decorator works on any function signature.
  • Always return the wrapped function's result from the wrapper.
  • Use functools.wraps(func) on your wrapper to preserve the original function's name and docstring (a small but professional touch).

Exercise & Challenge

Exercise 1 β€” Predict the output

Given this code
def outer():
    message = "hi"
    def inner():
        return message.upper()
    return inner
 
fn = outer()
print(fn())

What does this print?

πŸ† Challenge

Write a decorator called timer_label that prints '[LOG] Running <function name>' before calling the wrapped function, then apply it to a simple function.

  • Use *args/**kwargs so it works on any function
  • Print the function's name using func.__name__
  • Return the original function's result
πŸ’‘ Need a hint?

Every function object has a __name__ attribute.

πŸ” Show a sample solution
Python
def timer_label(func):
    def wrapper(*args, **kwargs):
        print(f"[LOG] Running {func.__name__}")
        return func(*args, **kwargs)
    return wrapper
 
@timer_label
def add(a, b):
    return a + b
 
print(add(2, 3))

Quiz

Answer every question, then submit to see your score and explanations.

1. What is a closure?

A function with no parameters
An inner function that retains access to variables from its enclosing scope
A way to close a file
A type of loop
Closures let an inner function 'remember' variables from the function that created it.

2. What does @my_decorator above a function definition actually do?

Nothing, it's just a comment
It replaces the function with my_decorator(function)
It deletes the function
It runs the function immediately
@decorator is shorthand for func = decorator(func).

3. Why should a decorator's wrapper accept *args and **kwargs?

It's required Python syntax
So it works regardless of what arguments the wrapped function takes
To make the code longer
It has no real purpose
Different functions take different arguments β€” *args/**kwargs makes the decorator universal.

Summary

  • A closure is an inner function that keeps access to variables from its enclosing function.
  • A decorator takes a function and returns a new function that wraps it with extra behavior.
  • @decorator above a def is shorthand for func = decorator(func).
  • Always forward *args/**kwargs and return the inner result in a decorator's wrapper.

Related lessons

Saved privately in your browser β€” no account needed.