Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Comparing Normal Functions and Generator Functions

Welcome to your first lesson. We'll begin our journey by exploring the fundamental building block for many of Python's advanced context-management features: the generator. Your goal is to understand how a shared object can know who is calling it, and the concept of pausing and resuming code is central to that.

This lesson focuses on the very first step: seeing the crucial difference between a regular function and a function containing the yield keyword. By the end, you will be able to compare what happens when you call each type of function, observing when their code runs and what value is returned from the call itself. This foundational concept will underpin everything we discuss later, from context managers to ContextVar.

The Immediate Execution of a Normal Function

Let's start with what you already know well: a standard Python function. When you call a normal function, its body executes immediately, from top to bottom, until it hits a return statement or the end of the function.

Consider this simple function. Run the following snippet and pay close attention to the order of the print outputs.

def normal_function():
    print("... Body of normal_function starts executing.")
    value_to_return = "Hello from a normal function!"
    print("... Body of normal_function is about to return.")
    return value_to_return
    # This line is never reached because of the return statement above.
    print("... This message will not appear.")

print("About to call normal_function...")
returned_value = normal_function()
print("Call to normal_function has finished.")
print(f"The returned value is: '{returned_value}'")
print(f"The type of the returned value is: {type(returned_value)}")

As you can see, the execution is linear and predictable:

  1. The code runs up to the function call.
  2. The function's body executes immediately.
  3. The function returns a value (a string, in this case).
  4. The code after the function call continues, using the returned value.

The key takeaway is that the call normal_function() runs the function and the expression evaluates to the result of that run.

The Suspended Execution of a Generator Function

Now, let's make a tiny change to that function. We will replace the return keyword with yield. This single change fundamentally alters the function's behavior. A function that contains yield anywhere in its body is called a generator function.

Run this next snippet. Compare the output carefully to the previous one.

def generator_function():
    print("... Body of generator_function starts executing.")
    value_to_yield = "Hello from a generator function!"
    print("... Body of generator_function is about to yield.")
    yield value_to_yield
    print("... Body of generator_function has resumed.")

print("About to call generator_function...")
returned_value = generator_function()
print("Call to generator_function has finished.")
print(f"The returned value is: {returned_value}")
print(f"The type of the returned value is: {type(returned_value)}")

Notice the stark difference in the output. When generator_function() was called, none of the print statements inside its body were executed. The function's code did not run. Instead, the call immediately returned a generator object.

This is the core concept of this lesson:

  • Calling a normal function executes its body and returns the result.
  • Calling a generator function does not execute its body. It creates and returns a generator object, which is essentially a suspended computation, ready to be started later.

To see this comparison made very clearly, please watch the first couple of minutes of the following video.

Python Tutorial: Generators - How to use them and the benefits you receive

This video by Corey Schafer provides a great visual demonstration of converting a regular function that returns a list into a generator.

First, watch how the initial square_numbers function works, returning a list (the regular function). Then, observe what happens when return is replaced with yield. Pay attention to what the function call returns now (the generator function). You can stop watching after he introduces the next() function, as we will cover that in our next lesson.

The author of the book "Asynchronous Programming in Python" also provides an excellent side-by-side code comparison that highlights this exact behavior.

Introduction - Asynchronous Programming in Python

This reading directly contrasts a function using return with an almost identical one using yield. The author uses print statements to make the execution flow obvious.

In the section yield is like return, please read the comparison between the two functions, print_return_and_print and print_yield_and_print. Study the code examples and the explanatory text that follows them, which walks through what happens when each function is called. You can stop at the subheading "Brevity has its costs".

This behavior of pausing execution is what makes generators so powerful. They allow a function to hand a value back to the caller (yield) and then wait, preserving their internal state, until the caller asks them to continue.

Key Takeaways

  • A normal Python function executes immediately upon being called and runs to completion, returning a value.
  • A function containing the yield keyword is a generator function.
  • Calling a generator function does not execute its body. Instead, it immediately returns a generator object.
  • This generator object represents a paused computation that can be started and resumed later.

In the next lesson, we will explore this generator object further. You will learn how to use the next() function to start the generator's execution, run it until the first yield statement, and see how it pauses, waiting for your next command.

End of Module,Exit lesson

Can't find a good explanation? Sign up and we'll make it for you

Sign up