Going DeeperPro· 40 min read

Iterators, Generators & Decorators

Produce values lazily one at a time with yield, and wrap extra behaviour around functions with decorators.

What you will learn

  • Write a generator with yield
  • Understand lazy, memory-friendly iteration
  • Read and write a simple decorator

The problem generators solve

Suppose you need the numbers 1 to ten million. Building a full list of them stores every value in memory at once — wasteful if you only ever look at one at a time. A generator produces values lazily: it hands you one value, pauses, and only computes the next when you ask. “Lazy” here means “does no work until needed.”

yield makes a generator

A generator looks like a normal function but uses yield instead of return. Each yield produces one value and pauses the function, remembering exactly where it was; the next request resumes from there.

yield produces values one at a time
def count_up_to(n):
    i = 1
    while i <= n:
        yield i          # produce one value, then pause
        i = i + 1

for number in count_up_to(3):
    print(number)

When the for loop asks for a value, count_up_to runs until it hits yield i, hands back i, and freezes. On the next loop turn it wakes up right after the yield, bumps i, and yields again — giving 1, then 2, then 3. It never builds a list; it makes each number on demand. For three values that is tiny, but for ten million it saves enormous memory.

Note: Output: 1 2 3

Tip: Any function with a yield in it is automatically a generator. Calling it does not run the body — it hands back a generator object that runs a little more each time you loop or call next() on it.

Decorators: wrap a function

A decorator is a function that wraps another function to add behaviour around it — without editing the original. This builds on two ideas you already know: a function is a value you can pass around, and a function can be defined inside another. Decorators power things like Flask and Django route definitions, so they are worth recognising.

A decorator adds behaviour around a function
def shout(func):
    def wrapper():
        result = func()
        return result.upper() + "!"
    return wrapper

@shout
def greet():
    return "hello"

print(greet())

Reading this: shout takes a function func and returns a new wrapper function that calls func, then uppercases its result and adds "!". The @shout line above greet is shorthand for “replace greet with shout(greet).” So when we call greet(), we are really running the wrapper: it calls the original ("hello"), transforms it, and returns HELLO!. The original greet was never edited — the decorator added behaviour around it.

Note: Output: HELLO!

In short, the two tools in this lesson both let you do more with less code:

  • A generator (yield) produces a sequence lazily, one item at a time, saving memory.
  • A decorator (@name) wraps a function to add behaviour around it, without changing the function itself.

Q. What keyword turns a function into a generator?

Answer: A function that uses yield becomes a generator — it produces values one at a time and pauses between them instead of returning once.

✍️ Practice

  1. Write a generator that yields the even numbers up to a limit.
  2. Write a decorator that prints "calling..." before running the wrapped function.

🏠 Homework

  1. Write a generator fibonacci(n) that yields the first n Fibonacci numbers, and loop over it to print them.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →