Static Typing

When writing an output for a given state, you can assume the finite state machine will be in that state. This might mean that specific object attributes will have values of speciifc types. Those attributes might, in general, be of some Union type: frequently, an Option type (which is a Union[T, None]).

It is an anti-pattern to check for these things inside the output. The reason for a state machine is for the outputs to avoid checking. However, if the output is type annotated, often mypy will complain that it cannot validate the types. The recommended solution is to assert the types inside the code. This aligns the assumptions mypy makes with the assumptions automat makes.

For example, consider the following:

import attr
import automat
from typing import Optional

@attr.s(auto_attribs=True)
class MaybeValue:
    _machine = automat.MethodicalMachine()
    _value: Optional[float] = attr.ib(default=None)

    @_machine.input()
    def set_value(self, value: float) -> None:
        "The value has been measured"

    @_machine.input()
    def get_value(self) -> float:
        "Return the value if it has been measured"

    @_machine.output()
    def _set_value_when_unset(self, value: float) -> None:
        self._value = value

    @_machine.output()
    def _get_value_when_set(self) -> float:
        """mypy will complain here:

        Incompatible return value type
        (got "Optional[float]", expected "float")
        """
        return self._value

    @_machine.state()
    def value_is_set(self):
        "The value is set"

    @_machine.state(initial=True)
    def value_is_unset(self):
        "The value is not set"

    value_is_unset.upon(
        set_value,
        enter=value_is_set,
        outputs=[_set_value_when_unset],
        collector=lambda x: None,
    )
    value_is_set.upon(
        get_value,
        enter=value_is_set,
        outputs=[_get_value_when_set],
        collector=lambda x: next(iter(x)),
    )

In this case starting _get_value_when_set with a line assert self._value is not None will satisfy mypy.