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():
- Runs until
yield - Returns the yielded value
- 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:
- Calls
next()to start—prints “Start”, gets timeout, pauses - Waits until time 5
- Calls
next()again—prints “Middle”, gets timeout, pauses - Waits until time 10
- 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.

