Python Generators Explained for SimPy Users

SimPy is built on Python generators. If generators feel mysterious, SimPy will too. Let's fix that.

What Is a Generator?

A generator is a function that can pause and resume. Instead of return, it uses yield.

def simple_generator():
    yield 1
    yield 2
    yield 3

Calling it doesn't run it—it creates a generator object:

gen = simple_generator()
print(type(gen))  # <class 'generator'>

Running a Generator

Use next() to advance to the next yield:

gen = simple_generator()
print(next(gen))  # 1
print(next(gen))  # 2
print(next(gen))  # 3
print(next(gen))  # StopIteration error

Each next(): 1. Runs until yield 2. Returns the yielded value 3. Pauses

Generators Remember State

This is the magic. Local variables persist:

def counter():
    count = 0
    while True:
        count += 1
        yield count

gen = counter()
print(next(gen))  # 1
print(next(gen))  # 2
print(next(gen))  # 3

count isn't reset between calls. The generator remembers where it was.

How SimPy Uses This

SimPy processes are generators that yield events:

def process(env):
    print("Start")
    yield env.timeout(5)  # Pause here
    print("Middle")
    yield env.timeout(5)  # Pause here
    print("End")

SimPy: 1. Calls next() to start—prints "Start", gets timeout, pauses 2. Waits until time 5 3. Calls next() again—prints "Middle", gets timeout, pauses 4. Waits until time 10 5. Calls next() again—prints "End", generator completes

The process maintains its state (local variables, position) between yields.

Yield Can Receive Values

Generators can receive values when resumed:

def echo():
    while True:
        received = yield
        print(f"Received: {received}")

gen = echo()
next(gen)          # Start the generator (runs to first yield)
gen.send("Hello")  # Received: Hello
gen.send("World")  # Received: World

SimPy uses this to pass event results back:

def process(env):
    # yield returns the event value when resumed
    result = yield some_process
    print(f"Process returned: {result}")

Generator Functions vs Regular Functions

Regular Function Generator Function
Runs to completion Can pause and resume
Uses return Uses yield
Returns a value Returns a generator object
Memory freed after return State preserved between yields

Creating Multiple Generators

Each call creates an independent generator:

def counter():
    count = 0
    while True:
        count += 1
        yield count

gen1 = counter()
gen2 = counter()

print(next(gen1))  # 1
print(next(gen1))  # 2
print(next(gen2))  # 1 (independent!)
print(next(gen1))  # 3

In SimPy, each process is an independent generator:

env.process(customer(env, "Alice"))  # One generator
env.process(customer(env, "Bob"))    # Different generator

Yield From

yield from delegates to another generator:

def inner():
    yield 1
    yield 2

def outer():
    yield from inner()  # Runs inner completely
    yield 3

gen = outer()
print(list(gen))  # [1, 2, 3]

In SimPy, use for sub-processes:

def prepare(env):
    yield env.timeout(5)

def execute(env):
    yield env.timeout(10)

def main(env):
    yield from prepare(env)
    yield from execute(env)

Generators Are Lazy

Generators compute values on demand:

def big_range():
    i = 0
    while True:
        yield i
        i += 1

gen = big_range()
# No memory used yet!
print(next(gen))  # 0 - computed now
print(next(gen))  # 1 - computed now

SimPy benefits from this—thousands of processes don't consume excessive memory.

Generator Methods

Generators have special methods:

gen = my_generator()
next(gen)           # Advance to next yield
gen.send(value)     # Resume with a value
gen.throw(Error)    # Throw exception at current yield
gen.close()         # Stop the generator

SimPy uses these internally to control processes.

Exception Handling in Generators

Generators handle exceptions normally:

def process():
    try:
        yield 1
        yield 2
    except ValueError:
        yield "Caught!"

gen = process()
print(next(gen))          # 1
print(gen.throw(ValueError))  # Caught!

SimPy uses this for interrupts:

def interruptible(env):
    try:
        yield env.timeout(100)
    except simpy.Interrupt:
        print("Interrupted!")

Practical Example: SimPy-Style Without SimPy

Here's a simplified simulation using pure generators:

import heapq

def process1():
    print("P1: Start at 0")
    yield 5
    print("P1: Resume at 5")
    yield 3
    print("P1: Resume at 8")

def process2():
    print("P2: Start at 0")
    yield 2
    print("P2: Resume at 2")
    yield 10
    print("P2: Resume at 12")

# Simple event loop
events = []  # (time, process)
now = 0

for p in [process1(), process2()]:
    delay = next(p)
    heapq.heappush(events, (now + delay, p))

while events:
    now, proc = heapq.heappop(events)
    try:
        delay = proc.send(None)
        heapq.heappush(events, (now + delay, proc))
    except StopIteration:
        pass

This is essentially what SimPy does, with more features.

Summary

Generators: - Are functions that pause (yield) and resume - Maintain state between pauses - Create independent instances on each call - Can receive values via send() - Are the foundation of SimPy processes

Understand generators. SimPy follows naturally.

Next Steps


Discover the Power of Simulation

Want to become a go-to expert in simulation with Python? The Complete Simulation Bootcamp will show you how simulation can transform your career and your projects.

Explore the Bootcamp