skills/simpy/references/events.md
Verified 2026-07-23 against SimPy 4.1.2 documentation and tagged source.
Environment owns a single event queue and processes one event at a time. Tagged
4.1.2 stores queue entries as (time, priority, event_id, event):
URGENT=0, NORMAL=1 in public event APIs);Thus same-time, same-priority events are FIFO by scheduling order. This is deterministic sequential execution, even when model processes represent concurrent activities. Floating-point discretization can collapse physically distinct times onto the same value, so test tie behavior explicitly.
An Event moves once through:
event.triggered is true;event.processed is true when
event.callbacks is None.import simpy
env = simpy.Environment()
event = env.event()
assert not event.triggered and not event.processed
event.succeed("ready")
assert event.triggered and not event.processed
env.step()
assert event.processed and event.value == "ready"
event.succeed(value) and event.fail(exception) return that event and may be
called only once. In 4.1.2 fail() requires an Exception. event.trigger(other)
copies the other event's success/failure and value, and returns None.
A failed event throws its exception into a waiting process. If no process or
callback defuses it, Environment.step() raises it. Treat private _ok, _value,
and _defused as implementation details.
Before processing, event.callbacks is a mutable list of one-argument callables.
Yielding an event adds the waiting process's resume method. Processing executes
callbacks in list order. Once fully processed, callbacks becomes None; appending
then is invalid.
log = []
timeout = env.timeout(2, value=7)
timeout.callbacks.append(lambda completed: log.append(completed.value))
env.run()
assert log == [7]
Keep callbacks short and non-blocking. A callback runs synchronously inside
Environment.step() and can affect scheduler latency.
env.timeout(delay, value=None) creates a Timeout, immediately triggers it, and
schedules it at env.now + delay. Because it is already triggered at construction,
do not call succeed() or fail() on it.
def timer(env):
result = yield env.timeout(3, value="elapsed")
assert result == "elapsed"
Reject negative delay. Use a consistent numeric time unit; SimPy does not attach units or protect against incompatible scales.
env.process(generator) requires a generator object, not an ordinary function
result. It schedules an urgent Initialize event. Each yielded event suspends the
generator; after the event's outcome, SimPy sends its value back or throws its
failure into the generator.
def child(env):
yield env.timeout(1)
return 42
def parent(env):
child_process = env.process(child(env))
result = yield child_process
assert result == 42
env = simpy.Environment()
env.process(parent(env))
env.run()
A Process is itself an event. It succeeds with the generator's return value or
fails with an uncaught exception. process.is_alive, process.target, and
process.name expose its current status.
Common mistakes:
env.process(worker) instead of env.process(worker(env));yield, which is not a generator;time.sleep() inside a normal Environment process;AnyOf(env, events) / a | b and AllOf(env, events) / a & b return a
Condition. Yielding one produces a ConditionValue, an ordered dict-like mapping
whose keys are the original Event objects and whose values are their values.
def coordinate(env):
fast = env.timeout(1, value="fast")
slow = env.timeout(2, value="slow")
first = yield fast | slow
assert fast in first
assert first[fast] == "fast"
both = yield fast & slow
assert list(both.items()) == [(fast, "fast"), (slow, "slow")]
For AllOf, all input events appear. For AnyOf, all target events that occurred
before the condition itself is processed can appear; do not assume exactly one
winner when events tie. Input order determines result order.
If any input event fails before the condition succeeds, AnyOf and AllOf fail.
Conditions can be nested. AnyOf does not cancel losing events:
with resource.request() as request:
patience = env.timeout(5)
result = yield request | patience
if request not in result:
# Context-manager exit cancels the still-pending request.
return
yield env.timeout(2)
An ordinary losing timeout remains scheduled. This is usually harmless but matters for event counts and traces.
Environment.run() boundariesenv.run() stops only when the event queue is empty. An endless generator such as
while True: yield env.timeout(1) makes it nonterminating.
env.run(until=10) creates an internal urgent stop event at time 10. It advances
env.now to 10 but does not process ordinary events scheduled exactly at 10:
env = simpy.Environment()
boundary = env.timeout(10)
env.run(until=10)
assert env.now == 10
assert not boundary.processed
The numeric target must be strictly greater than env.now.
env.run(until=event) attaches a stop callback and returns the event value when
that callback fires. It raises RuntimeError if the schedule empties before the
criterion is triggered.
Important 4.1.2 implementation detail: Environment.step() catches
StopSimulation, preserves callbacks after the stopping callback, and reschedules
the event at priority -1. Since processed means callbacks is None, the target
can still report processed == False immediately after run() returns:
env = simpy.Environment()
target = env.timeout(5, value="done")
assert env.run(until=target) == "done"
assert target.triggered
assert not target.processed
env.step()
assert target.processed
This behavior follows tagged 4.1.2 simpy/core.py; the topical guide's informal
"processed" wording is not a safe postcondition. Depend on the returned value and
your model state, not solely on processed.
A timeout event and a numeric time can reach the same clock value but differ in same-time ordering. Numeric stopping is the clearer half-open horizon.
until = 10
steps = 0
max_steps = 100_000
while env.peek() < until and steps < max_steps:
env.step()
steps += 1
if steps == max_steps:
raise RuntimeError("event budget reached")
peek() returns infinity when empty; step() raises simpy.core.EmptySchedule
when no event remains. Explicit step/event caps prevent a zero-delay loop from
hanging diagnostics.
See sources.md for versioned environment/event guides, API references, tagged
core.py, and the scheduling guide.