Weighted transitions¶
See also
See Conditions for how the engine selects transitions, and Actions for callbacks that run during transitions.
The weighted_transitions utility lets you define probabilistic
transitions — where each transition from a state has a relative weight
that determines how likely it is to be selected when the event fires.
This is a contrib module that works entirely through the existing Conditions system. No engine modifications are needed.
Basic usage¶
Import weighted_transitions and pass a source state followed by
(target, weight) tuples. The result is a regular TransitionList
that you assign to a class attribute as an event:
>>> from statemachine.contrib.weighted import to, weighted_transitions
>>> class GameCharacter(StateChart):
... standing = State(initial=True)
... shift_weight = State()
... adjust_hair = State()
... bang_shield = State()
...
... idle = weighted_transitions(
... standing,
... (shift_weight, 70),
... (adjust_hair, 20),
... (bang_shield, 10),
... seed=42,
... )
...
... finish = (
... shift_weight.to(standing)
... | adjust_hair.to(standing)
... | bang_shield.to(standing)
... )
>>> sm = GameCharacter()
>>> sm.send("idle")
>>> any(
... s in sm.configuration_values
... for s in ("shift_weight", "adjust_hair", "bang_shield")
... )
True
When idle fires, the engine randomly selects one of the three transitions based on
their relative weights: 70% chance for shift_weight, 20% for adjust_hair,
10% for bang_shield.
Weights¶
Weights can be any positive number — integers, floats, or a mix of both. They are relative, not absolute percentages:
# These are equivalent (same 70/20/10 ratio):
idle = weighted_transitions(
standing,
(shift_weight, 70),
(adjust_hair, 20),
(bang_shield, 10),
)
idle = weighted_transitions(
standing,
(shift_weight, 7),
(adjust_hair, 2),
(bang_shield, 1),
)
idle = weighted_transitions(
standing,
(shift_weight, 0.7),
(adjust_hair, 0.2),
(bang_shield, 0.1),
)
The tuple format (target, weight) follows the standard Python pattern used by
random.choices().
Reproducibility with seed¶
Pass a seed parameter for deterministic, reproducible sequences — useful for testing:
go = weighted_transitions(
s1,
(s2, 50),
(s3, 50),
seed=42, # same seed always produces the same sequence
)
Note
The seed initializes a per-group random.Random instance that is shared across all
instances of the same state machine class. This means the sequence is deterministic
for a given program execution, but different instances advance the same RNG.
Per-transition options¶
Use the to() helper to pass transition keyword
arguments (cond, unless, before, on, after, …) as natural kwargs.
For simple destinations without extra options, a plain (target, weight) tuple is
enough — to() is only needed when you want to customize the transition:
>>> class GuardedWeighted(StateChart):
... idle = State(initial=True)
... walk = State()
... run = State()
...
... move = weighted_transitions(
... idle,
... (walk, 70),
... to(run, 30, cond="has_energy"),
... )
... stop = walk.to(idle) | run.to(idle)
...
... has_energy = True
>>> sm = GuardedWeighted()
Important
No fallback when a guard fails. If the weighted selection picks a transition whose
guard evaluates to False, the event fails — the engine does not silently fall back
to another transition. This preserves the probability semantics: a 70/30 split means
exactly that, not “70/30 unless the 30% is blocked, in which case always 100% for
the other”.
This behavior follows Conditions evaluation: the first transition whose all conditions pass is executed.
Combining with callbacks¶
All standard Actions work with weighted events — before, on, after callbacks
and naming conventions like on_<event>():
class WithCallbacks(StateChart):
s1 = State(initial=True)
s2 = State()
s3 = State()
go = weighted_transitions(s1, (s2, 60), (s3, 40))
back = s2.to(s1) | s3.to(s1)
def on_go(self):
print("go event fired!")
def after_go(self):
print("after go!")
Multiple independent groups¶
Each call to weighted_transitions() creates an independent weighted group with its
own RNG. You can have multiple weighted events on the same state machine:
class MultiGroup(StateChart):
idle = State(initial=True)
walk = State()
run = State()
wave = State()
bow = State()
move = weighted_transitions(idle, (walk, 70), (run, 30), seed=1)
greet = weighted_transitions(idle, (wave, 80), (bow, 20), seed=2)
back = walk.to(idle) | run.to(idle) | wave.to(idle) | bow.to(idle)
The move and greet events use separate RNGs and don’t interfere with each other.
Validation¶
weighted_transitions() validates inputs at class definition time:
The first argument must be a
State(the source).Each destination must be a
(target_state, weight)or(target_state, weight, kwargs_dict)tuple.Weights must be positive numbers (
intorfloat).At least one destination is required.
>>> weighted_transitions(State(initial=True))
Traceback (most recent call last):
...
ValueError: weighted_transitions() requires at least one (target, weight) destination
>>> s1, s2 = State(initial=True), State()
>>> weighted_transitions(s1, (s2, -5))
Traceback (most recent call last):
...
ValueError: Destination 0: weight must be positive, got -5
>>> weighted_transitions(s1, (s2, "ten"))
Traceback (most recent call last):
...
TypeError: Destination 0: weight must be a positive number, got str
How it works¶
Under the hood, weighted_transitions():
Creates a
_WeightedGroupholding the weights and arandom.Randominstance.Calls
source.to(target, **kwargs)for each destination, creating standard transitions.Attaches a lightweight condition callable to each transition’s
condlist.When the event fires, the engine evaluates conditions in order. The first condition to run rolls the dice (using
random.choices) and caches the result. Subsequent conditions check against the cache.Only the selected transition’s condition returns
True— the engine picks it.
This means weighted transitions are fully compatible with all engine features: Actions, Conditions, Listeners, async engines, and diagram generation.