Python decorators and generators matters once your pages get past the basics. Two features that wrap functions and produce values lazily.
Understanding decorators
A decorator is a function that wraps another function to change its behaviour without editing the original. It gives you a tidy way to call higher-order functions.
Basic decorator
Start with a simple example:
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
def say_hello():
print("Hello!")
# Decorate the function
say_hello = my_decorator(say_hello)
say_hello()
When you call say_hello(), it prints a message, runs the original function, then prints another message.
The @ syntax
Python also lets you apply decorators with the @ symbol – syntactic sugar that reads more clearly:
@my_decorator
def say_hello():
print("Hello!")
say_hello()
That is the same as say_hello = my_decorator(say_hello), just shorter.
Understanding generators
Generators are a simple way to build iterators. You write them like normal functions, but they use yield to return values one at a time. Each call to yield produces the next value, which makes them work in a for loop.
Creating a generator
Here is a minimal example:
def my_generator():
yield 1
yield 2
yield 3
g = my_generator()
for value in g:
print(value)
Output:
1
2
3
Why use generators?
Generators compute values on demand instead of building a full list in memory. That matters when you are working with large datasets.
Generator expressions
Like list comprehensions, but with round parentheses () instead of square brackets []:
my_gen = (x * x for x in range(3))
for x in my_gen:
print(x)
That prints 0, 1, and 4 – the squares of 0 to 2.
Combining decorators and generators
You can use both together. For example, a decorator can time how long a generator function takes:
import time
def timing_function(some_function):
"""
Outputs the time a function takes
to execute.
"""
def wrapper():
t1 = time.time()
some_function()
t2 = time.time()
return f"Time it took to run the function: {t2 - t1} \n"
return wrapper
@timing_function
def my_generator():
num_list = []
for num in (x * x for x in range(10000)): # Generator expression
num_list.append(num)
print("\nSum of squares: ", sum(num_list))
my_generator()
Decorators change how functions behave. Generators produce values lazily. Both turn up often once you move past beginner Python – worth trying in small scripts until they feel familiar.

