Finite State Machine#

A finite-state machine (FSM) or finite-state automaton (FSA, plural: automata), finite automaton, or simply a state machine, is a mathematical model of computation.

It is an abstract machine that can be in exactly one of a finite number of states at any given time. The FSM can change from one state to another in response to some inputs; the change from one state to another is called a transition.

An FSM is defined by a list of its states, its initial state, and the inputs that trigger each transition.


Source: WikiPedia

Usage example#

Not all functionality of the bot can be implemented as single handler, for example you will need to collect some data from user in separated steps you will need to use FSM.

FSM Example

Let’s see how to do that step-by-step

Step by step#

Before handle any states you will need to specify what kind of states you want to handle

15
16class Form(StatesGroup):
17    name = State()
18    like_bots = State()

And then write handler for each state separately from the start of dialog

Here is dialog can be started only via command /start, so lets handle it and make transition user to state Form.name

21
22@form_router.message(Command(commands=["start"]))
23async def command_start(message: Message, state: FSMContext) -> None:
24    await state.set_state(Form.name)
25    await message.answer(
26        "Hi there! What's your name?",
27        reply_markup=ReplyKeyboardRemove(),

After that you will need to save some data to the storage and make transition to next step.

48
49@form_router.message(Form.name)
50async def process_name(message: Message, state: FSMContext) -> None:
51    await state.update_data(name=message.text)
52    await state.set_state(Form.like_bots)
53    await message.answer(
54        f"Nice to meet you, {html.quote(message.text)}!\nDid you like to write bots?",
55        reply_markup=ReplyKeyboardMarkup(
56            keyboard=[
57                [
58                    KeyboardButton(text="Yes"),
59                    KeyboardButton(text="No"),
60                ]
61            ],
62            resize_keyboard=True,
63        ),

At the next steps user can make different answers, it can be yes, no or any other

Handle yes and soon we need to handle Form.language state

77
78@form_router.message(Form.like_bots, F.text.casefold() == "yes")
79async def process_like_write_bots(message: Message, state: FSMContext) -> None:
80    await state.set_state(Form.language)
81
82    await message.reply(
83        "Cool! I'm too!\nWhat programming language did you use for it?",
84        reply_markup=ReplyKeyboardRemove(),

Handle no

66
67@form_router.message(Form.like_bots, F.text.casefold() == "no")
68async def process_dont_like_write_bots(message: Message, state: FSMContext) -> None:
69    data = await state.get_data()
70    await state.clear()
71    await message.answer(
72        "Not bad not terrible.\nSee you soon.",
73        reply_markup=ReplyKeyboardRemove(),
74    )

And handle any other answers

87
88@form_router.message(Form.like_bots)
89async def process_unknown_write_bots(message: Message, state: FSMContext) -> None:

All possible cases of like_bots step was covered, let’s implement finally step

 92
 93@form_router.message(Form.language)
 94async def process_language(message: Message, state: FSMContext) -> None:
 95    data = await state.update_data(language=message.text)
 96    await state.clear()
 97    text = (
 98        "Thank for all! Python is in my hearth!\nSee you soon."
 99        if message.text.casefold() == "python"
100        else "Thank for information!\nSee you soon."
101    )
102    await message.answer(text)

And now you have covered all steps from the image, but you can make possibility to cancel conversation, lets do that via command or text

30
31@form_router.message(Command(commands=["cancel"]))
32@form_router.message(F.text.casefold() == "cancel")
33async def cancel_handler(message: Message, state: FSMContext) -> None:
34    """
35    Allow user to cancel any action
36    """
37    current_state = await state.get_state()
38    if current_state is None:
39        return
40
41    logging.info("Cancelling state %r", current_state)
42    await state.clear()
43    await message.answer(
44        "Cancelled.",
45        reply_markup=ReplyKeyboardRemove(),

Complete example#

  1import asyncio
  2import logging
  3import sys
  4from os import getenv
  5from typing import Any, Dict
  6
  7from aiogram import Bot, Dispatcher, F, Router, html
  8from aiogram.filters import Command
  9from aiogram.fsm.context import FSMContext
 10from aiogram.fsm.state import State, StatesGroup
 11from aiogram.types import KeyboardButton, Message, ReplyKeyboardMarkup, ReplyKeyboardRemove
 12
 13form_router = Router()
 14
 15
 16class Form(StatesGroup):
 17    name = State()
 18    like_bots = State()
 19    language = State()
 20
 21
 22@form_router.message(Command(commands=["start"]))
 23async def command_start(message: Message, state: FSMContext) -> None:
 24    await state.set_state(Form.name)
 25    await message.answer(
 26        "Hi there! What's your name?",
 27        reply_markup=ReplyKeyboardRemove(),
 28    )
 29
 30
 31@form_router.message(Command(commands=["cancel"]))
 32@form_router.message(F.text.casefold() == "cancel")
 33async def cancel_handler(message: Message, state: FSMContext) -> None:
 34    """
 35    Allow user to cancel any action
 36    """
 37    current_state = await state.get_state()
 38    if current_state is None:
 39        return
 40
 41    logging.info("Cancelling state %r", current_state)
 42    await state.clear()
 43    await message.answer(
 44        "Cancelled.",
 45        reply_markup=ReplyKeyboardRemove(),
 46    )
 47
 48
 49@form_router.message(Form.name)
 50async def process_name(message: Message, state: FSMContext) -> None:
 51    await state.update_data(name=message.text)
 52    await state.set_state(Form.like_bots)
 53    await message.answer(
 54        f"Nice to meet you, {html.quote(message.text)}!\nDid you like to write bots?",
 55        reply_markup=ReplyKeyboardMarkup(
 56            keyboard=[
 57                [
 58                    KeyboardButton(text="Yes"),
 59                    KeyboardButton(text="No"),
 60                ]
 61            ],
 62            resize_keyboard=True,
 63        ),
 64    )
 65
 66
 67@form_router.message(Form.like_bots, F.text.casefold() == "no")
 68async def process_dont_like_write_bots(message: Message, state: FSMContext) -> None:
 69    data = await state.get_data()
 70    await state.clear()
 71    await message.answer(
 72        "Not bad not terrible.\nSee you soon.",
 73        reply_markup=ReplyKeyboardRemove(),
 74    )
 75    await show_summary(message=message, data=data, positive=False)
 76
 77
 78@form_router.message(Form.like_bots, F.text.casefold() == "yes")
 79async def process_like_write_bots(message: Message, state: FSMContext) -> None:
 80    await state.set_state(Form.language)
 81
 82    await message.reply(
 83        "Cool! I'm too!\nWhat programming language did you use for it?",
 84        reply_markup=ReplyKeyboardRemove(),
 85    )
 86
 87
 88@form_router.message(Form.like_bots)
 89async def process_unknown_write_bots(message: Message, state: FSMContext) -> None:
 90    await message.reply("I don't understand you :(")
 91
 92
 93@form_router.message(Form.language)
 94async def process_language(message: Message, state: FSMContext) -> None:
 95    data = await state.update_data(language=message.text)
 96    await state.clear()
 97    text = (
 98        "Thank for all! Python is in my hearth!\nSee you soon."
 99        if message.text.casefold() == "python"
100        else "Thank for information!\nSee you soon."
101    )
102    await message.answer(text)
103    await show_summary(message=message, data=data)
104
105
106async def show_summary(message: Message, data: Dict[str, Any], positive: bool = True) -> None:
107    name = data["name"]
108    language = data.get("language", "<something unexpected>")
109    text = f"I'll keep in mind that, {html.quote(name)}, "
110    text += (
111        f"you like to write bots with {html.quote(language)}."
112        if positive
113        else "you don't like to write bots, so sad..."
114    )
115    await message.answer(text=text, reply_markup=ReplyKeyboardRemove())
116
117
118async def main():
119    bot = Bot(token=getenv("TELEGRAM_TOKEN"), parse_mode="HTML")
120    dp = Dispatcher()
121    dp.include_router(form_router)
122
123    await dp.start_polling(bot)
124
125
126if __name__ == "__main__":
127    logging.basicConfig(level=logging.INFO, stream=sys.stdout)
128    asyncio.run(main())

Read more#