(validators-and-guards)= (validators and guards)=
Conditions and Validations are checked before a transition is started. They are meant to prevent or stop a transition to occur.
The main difference is that {ref}validators raise exceptions to stop the flow, and {ref}conditions
act like predicates that shall resolve to a boolean value.
Please see {ref}`dynamic-dispatch` to know more about how this lib supports multiple signatures
for all the available callbacks, being validators and guards or {ref}`actions`.
(guards)=
This feature is also known as a Conditional Transition.
A conditional transition occurs only if specific conditions or criteria are met. In addition to checking if there is a transition handling the event in the current state, you can register callbacks that are evaluated based on other factors or inputs at runtime.
When a transition is conditional, it includes a condition (also known as a guard) that must be satisfied for the transition to take place. If the condition is not met, the transition does not occur, and the state machine remains in its current state or follows an alternative path.
This feature allows for multiple transitions on the same {ref}event, with each {ref}transition checked in declaration order — that is, the order in which the transitions themselves were created using state.to(). A condition acts like a predicate (a function that evaluates to true/false) and is checked when a {ref}statemachine handles an {ref}event with a transition from the current state bound to this event. The first transition that meets the conditions (if any) is executed. If none of the transitions meet the conditions, the state machine either raises an exception or does nothing (see the allow_event_without_transition parameter of {ref}StateMachine).
**Evaluation order is based on declaration order, not composition order.**
When using conditional transitions, the order of evaluation is determined by **when each transition was created** (the order of `state.to()` calls), **not** by the order they appear when combined with the `|` operator.
For example:
```python
# These are evaluated in DECLARATION ORDER (when state.to() was called):
created_first = state_a.to(state_x) # Created FIRST → Checked FIRST
created_second = state_a.to(state_y) # Created SECOND → Checked SECOND
created_third = state_a.to(state_z) # Created THIRD → Checked THIRD
# The | operator does NOT change evaluation order:
my_event = created_third | created_second | created_first
# Evaluation order is still: created_first → created_second → created_third
```
To control the evaluation order, declare transitions in the desired order:
```python
# Declare in the order you want them checked:
first = state_a.to(state_b, cond="check1") # Checked FIRST
second = state_a.to(state_c, cond="check2") # Checked SECOND
third = state_a.to(state_d, cond="check3") # Checked THIRD
my_event = first | second | third # Order matches declaration
```
When {ref}transitions have guards, it is possible to define two or more transitions for the same {ref}event from the same {ref}state. When the {ref}event occurs, the guarded transitions are checked one by one, and the first transition whose guard is true will be executed, while the others will be ignored.
A condition is generally a boolean function, property, or attribute, and must not have any side effects. Side effects are reserved for {ref}actions.
There are two variations of Guard clauses available:
cond
: A list of condition expressions, acting like predicates. A transition is only allowed to occur if
all conditions evaluate to True.
- Single condition expression:
cond="condition"/cond="<condition expression>" - Multiple condition expressions:
cond=["condition1", "condition2"]
unless
: Same as cond, but the transition is only allowed if all conditions evaluate to False.
- Single condition:
unless="condition"/unless="<condition expression>" - Multiple conditions:
unless=["condition1", "condition2"]
This library supports a mini-language for boolean expressions in conditions, allowing the definition of guards that control transitions based on specified criteria. It includes basic boolean algebra operators, parentheses for controlling precedence, and names that refer to attributes on the state machine, its associated model, or registered {ref}Listeners.
All condition expressions are evaluated when the State Machine is instantiated. This is by design to help you catch any invalid definitions early, rather than when your state machine is running.
The mini-language is based on Python's built-in language and the ast parser, so there are no surprises if you’re familiar with Python. Below is a formal specification to clarify the structure.
-
Names:
- Names refer to attributes on the state machine instance, its model or listeners, used directly in expressions to evaluate conditions.
- Names must consist of alphanumeric characters and underscores (
_) and cannot begin with a digit (e.g.,is_active,count,has_permission). - Any property name used in the expression must exist as an attribute on the state machine, model instance, or listeners, otherwise, an
InvalidDefinitionerror is raised. - Names can be pointed to
properties,attributesormethods. If pointed toattributes, the library will create a wrapper get method so each time the expression is evaluated the current value will be retrieved.
-
Boolean operators and precedence:
- The following Boolean operators are supported, listed from highest to lowest precedence:
not/!— Logical negationand/^— Logical conjunctionor/v— Logical disjunctionor/v— Logical disjunction
- These operators are case-sensitive (e.g.,
NOTandNotare not equivalent tonotand will raise syntax errors). - Both formats can be used interchangeably, so
!sauron_aliveandnot sauron_aliveare equivalent.
- The following Boolean operators are supported, listed from highest to lowest precedence:
-
Comparisson operators:
- The following comparison operators are supported:
>— Greather than.>=— Greather than or equal.==— Equal.!=— Not equal.<— Lower than.<=— Lower than or equal.
- All comparison operations in Python have the same priority.
- The following comparison operators are supported:
-
Parentheses for precedence:
- When operators with the same precedence appear in the expression, evaluation proceeds from left to right, unless parentheses specify a different order.
- Parentheses
(and)are supported to control the order of evaluation in expressions. - Expressions within parentheses are evaluated first, allowing explicit precedence control (e.g.,
(is_admin or is_moderator) and has_permission).
Examples of valid boolean expressions include:
is_logged_in and has_permissionnot is_active or is_admin!(is_guest ^ has_access)(is_admin or is_moderator) and !is_bannedhas_account and (verified or trusted)frodo_has_ring and gandalf_present or !sauron_alive
Being used on a transition definition:
start.to(end, cond="frodo_has_ring and gandalf_present or !sauron_alive")See {ref}`sphx_glr_auto_examples_lor_machine.py` for an example of
using boolean algebra in conditions.
See {ref}`sphx_glr_auto_examples_air_conditioner_machine.py` for an example of
combining multiple transitions to the same event.
In Python, a boolean value is either `True` or `False`. However, there are also specific values that
are considered "**falsy**" and will evaluate as `False` when used in a boolean context.
These include:
1. The special value `None`.
2. Numeric values of `0` or `0.0`.
3. **Empty** strings, lists, tuples, sets, and dictionaries.
4. Instances of certain classes that define a `__bool__()` or `__len__()` method that returns
`False` or `0`, respectively.
On the other hand, any value that is not considered "**falsy**" is considered "**truthy**" and will evaluate to `True` when used in a boolean context.
So, a condition `s1.to(s2, cond=lambda: [])` will evaluate as `False`, as an empty list is a
**falsy** value.
The {ref}StateMachine.allowed_events property returns events reachable from the current state,
but it does not evaluate cond/unless guards. To check which events actually have their
conditions satisfied, use {ref}StateMachine.enabled_events.
>>> from statemachine import StateMachine, State
>>> class ApprovalMachine(StateMachine):
... pending = State(initial=True)
... approved = State(final=True)
... rejected = State(final=True)
...
... approve = pending.to(approved, cond="is_manager")
... reject = pending.to(rejected)
...
... is_manager = False
>>> sm = ApprovalMachine()
>>> [e.id for e in sm.allowed_events]
['approve', 'reject']
>>> [e.id for e in sm.enabled_events()]
['reject']
>>> sm.is_manager = True
>>> [e.id for e in sm.enabled_events()]
['approve', 'reject']enabled_events is a method (not a property) because conditions may depend on runtime
arguments. Any *args/**kwargs passed to enabled_events() are forwarded to the
condition callbacks, just like when triggering an event:
>>> class TaskMachine(StateMachine):
... idle = State(initial=True)
... running = State(final=True)
...
... start = idle.to(running, cond="has_enough_resources")
...
... def has_enough_resources(self, cpu=0):
... return cpu >= 4
>>> sm = TaskMachine()
>>> sm.enabled_events()
[]
>>> [e.id for e in sm.enabled_events(cpu=8)]
['start']This is useful for UI scenarios where you want to show or hide buttons based on whether
an event's conditions are currently satisfied.
An event is considered **enabled** if at least one of its transitions from the current state
has all conditions satisfied. If a condition raises an exception, the event is treated as
enabled (permissive behavior).
Are like {ref}guards, but instead of evaluating to boolean, they are expected to raise an
exception to stop the flow. It may be useful for imperative-style programming when you don't
want to continue evaluating other possible transitions and exit immediately.
- Single validator:
validators="validator" - Multiple validator:
validators=["validator1", "validator2"]
Both conditions and validators can also be combined for a single event.
<event> = <state1>.to(<state2>, cond="condition1", unless="condition2", validators="validator")
Consider this example:
class InvoiceStateMachine(StateMachine):
unpaid = State(initial=True)
paid = State()
failed = State()
paused = False
offer_valid = True
pay = (
unpaid.to(paid, cond="payment_success") |
unpaid.to(failed, validators="validator", unless="paused") |
failed.to(paid, cond=["payment_success", "offer_valid"])
)
def payment_success(self, event_data):
return <condition logic goes here>
def validator(self):
return <validator logic goes here>See the example {ref}`sphx_glr_auto_examples_all_actions_machine.py` for a complete example of
validators and guards.
Reference: Statecharts.