Знакомство с aiogram¶
Используемая версия aiogram: 3.27.0
Некоторые детали сознательно упрощены!
Я убеждён, что помимо теории должна быть и практика. Чтобы читателю было проще повторить описанные далее примеры, пришлось пойти на использование подходов, пригодных только для локальной разработки и обучения.
Или иногда в качестве хранилищ данных будут использоваться структуры, расположенные исключительно в оперативной памяти (словари, списки...). В действительности такие объекты нежелательны, поскольку остановка бота приведёт безвозвратной потере данных.
Также механизмом получения апдейтов от Telegram выбран поллинг, поскольку он гарантированно работает в подавляющем большинстве окружений и подходит практически всем разработчикам.
Важно помнить, что я ставлю перед собой цель объяснить именно работу с Telegram Bot API при помощи aiogram, а не вообще весь Computer Science во всём его многообразии.
Терминология¶
Введём некоторые термины, чтобы в дальнейшем не путаться:
- ЛС — личные сообщения, в контексте бота это диалог один-на-один с пользователем, а не группа/канал.
- Чат — общее название для ЛС, групп, супергрупп и каналов.
- Апдейт — любое событие из этого списка: сообщение, редактирование сообщения, колбэк, инлайн-запрос, платёж, добавление бота в группу и т.д.
- Хэндлер — асинхронная функция, которая получает от диспетчера/роутера очередной апдейт и обрабатывает его.
- Диспетчер — объект, занимающийся получением апдейтов от Telegram с последующим выбором хэндлера для обработки принятого апдейта.
- Роутер — аналогично диспетчеру, но отвечает за подмножество множества хэндлеров.
- Фильтр — выражение, которое обычно возвращает True или False и влияет на то, будет вызван хэндлер или нет.
- Мидлварь — прослойка, которая вклинивается в обработку апдейтов.
Установка¶
На момент написания этой главы (2026 год) популярным пакетным менеджером для Python-приложений является uv, демонстрационные проекты на GitHub будут постепенно переведены на него.
Если вы скачали имеющийся проект, который уже использует uv, то всё, что нужно сделать, это выполнить uv sync,
пакетный менеджер самостоятельно скачает нужную версию Python при необходимости и поставит все пакеты
из pyproject.toml, однако если вы создаёте проект с нуля, то выполните последовательно следующие команды:
uv init --python 3.14
uv add aiogram
uv run python -c "import aiogram"
В качестве версии Python можно указать любую актуальную и получившую хотя бы одно-два патч-исправления
(например, 3.14.2 или 3.15.1). Если после uv run python -c "import aiogram" в терминале не было ошибок,
значит, вы всё установили правильно.
Первый бот¶
Асинхронное программирование в Python
aiogram – это асинхронная библиотека, поэтому дальше без опыта работы с asyncio будет сложно.
Прекрасный туториал по асинхронности доступен
на сайте Python.
В учебных целях напишем самого-самого простого однофайлового бота, чтобы понять, что такое aiogram и как происходит взаимодействие пользователя с ботом. Сразу предупрежу: держать весь код в одном файле очень быстро становится плохой идеей, но хорошо подойдёт в двух случаях: демонстрационный код и одноразовый бот, например, чтобы быстро что-то проверить. Так или иначе, далее в этой главе мы напишем уже более продвинутого бота с нормальной файловой структурой.
В нашем первом боте будет всего лишь один обработчик: на любое сообщение он будет отправлять легендарное "Hello world!". Что вообще из себя представляет обработчик (хэндлер)? Это асинхронная функция, которая получает на вход объект какого-либо события от Telegram (сообщение, колбэк и т.д.) и что-то с этим событием делает. Чтобы aiogram знал, что такая-то функция заявляет себя как обработчик события, её надо зарегистрировать, т.е. как-то привязать к aiogram. Но привязка эта происходит не к самому фреймворку, а к отдельному объекту в нём – диспетчеру.
Создайте в каталоге проекта файл single_file_bot.py со следующим текстом:
import asyncio # [1]
from os import getenv # [1]
from aiogram import Bot, Dispatcher # [1]
from aiogram.types import Message # [1]
dp = Dispatcher() # [2]
@dp.message() # [3]
async def any_message( # [4]
message: Message, # [5]
):
await message.answer("Hello world!") # [6]
async def main():
token = getenv("BOT_TOKEN") # [7]
if not token: # [7]
error = "No token provided" # [7]
raise ValueError(error) # [7]
bot = Bot(token=token) # [8]
print("Starting bot...")
try:
await dp.start_polling(bot) # [9]
finally:
print("Bot stopped")
if __name__ == '__main__':
asyncio.run(main())
Очень компактный код, и в почти каждой строчке заложен смысл. Цифрами в комментариях обозначены:
- Импорты.
asyncioобязателен для запуска,getenvпозволит прочитать токен из переменных окружения. Из импортов из aiogram: первые два (Bot,Dispatcher) обязательны, последний (Message) нужен только для типизации и подсказок в IDE. Очень рекомендую не пренебрегать типизацией, это упрощает как написание кода, так и его чтение и поддержку. - Создание объекта диспетчера. В этом примере он создаётся на уровне всего модуля, поскольку на него будет далее зарегистрирован хэндлер.
- Декоратор, который читается как «функция под этим декоратором зарегистрирована в роутере
routerдля обработки апдейтов типаMessage(апдейт лежит в полеmessageу классаUpdateапдейта, который приходит от Telegram)». - Объявление функции, которая будет обрабатывать апдейт типа
Message. - Параметр, в котором при вызове функции будет лежать тело апдейта (в данном случае – типа
Message). У хэндлеров всегда есть как минимум один параметр (может быть больше), а тип значения этого параметра зависит от того, на какой тип зарегистрирована функция. - Бизнес-логика хэндлера. В данном случае из этого обработчика производится отправка сообщения в тот же чат, где пришло исходное событие. Подробнее об отправке сообщений поговорим в следующей главе.
- Чтение токена из переменной окружения
BOT_TOKEN. В случае, если переменная не задана или в ней лежит пустое значение, выкидываем ошибку. - Создание объекта бота с ранее полученным токеном.
- Запуск бота в режиме поллинга.
Запустите бота, передав в качестве переменной окружения реальный токен, полученный у @BotFather. Вы должны увидеть сообщение о старте и сам бот на любое сообщение будет присылать текст "Hello world!":
$ BOT_TOKEN=1234567890:AaBbCcDdEeFfGrOoShAHhIiJjKkLlMmNnOo uv run single_file_bot.py

Поздравляю: ваш первый бот готов! Но, как и было сказано ранее, держать весь код в одном файле – не самая лучшая идея. Поэтому быстренько похлопаем себя по плечу за hello world и будем делать по-нормальному.
Каркас бота¶
За годы работы с Telegram Bot API у меня выработался определённый стиль написания кода в целом и телеграм-ботов в частности, поэтому код в главах будет опираться именно на него. Здесь важно отметить, что идеала не существует и вы вольны свои исходники оформлять как угодно, особенно если этот код никому больше показывать не будете.
Вот, например, как выглядит упрощённая структура файлов моего типичного бота:
.
├── README.md
├── alembic
│ └── <файлы с миграциями>
├── alembic.ini
├── bot
│ ├── __init__.py
│ ├── __main__.py
│ ├── config.py
│ ├── db
│ │ └── <разные файлы>
│ ├── handlers
│ │ ├── __init__.py
│ │ └── <разные файлы>
│ ├── i18n
│ │ └── <разные файлы>
│ ├── logging_config.py
│ └── middlewares
│ ├── __init__.py
│ └── <разные файлы>
├── settings.example.toml
├── settings.toml
├── pyproject.toml
└── uv.lock
Выше я опустил различные служебные файлы и каталоги, например, Makefile, каталог deploy со скриптами
для разворачивания приложения на сервере, тесты и прочее. Поговорим про то, что осталось на структуре:
config.py: тут описана конфигурация бота: токены, DSN для подключения к базе данных, параметры логов,
различные внешние API-ключи и т.д. В последние годы я предпочитаю формат TOML для хранения настроек,
у него нет «болезни отступов», свойственной YAML, данные легко группировать по секциям,
и сам формат считается «родным» в Python 3.12+. Но поскольку в мои open-source проекты периодически приходили
комментаторы с вопросом, как запустить моих ботов, имея из конфигурируемых параметров только переменные окружения,
я теперь добавляю fallback в виде поддержки env vars в свой код. Например, если в settings.toml написано:
[bot]
token = "1234567890:AaBbCcDdEeFfGrOoShAHhIiJjKkLlMmNnOo"
То указать то же самое значение без TOML-файла, но через переменные окружения можно так:
# двойное подчёркивание как разделитель задано в config.py
BOT__TOKEN=1234567890:AaBbCcDdEeFfGrOoShAHhIiJjKkLlMmNnOo
logging_config.py: для логирования я давно использую structlog, потому что
JSON-логи – это очень удобно и позволяет парсить значения автоматизированными средствами.
В своих ботах я собираю как логи, так и различные метрики с исключениями, всё это затем улетает в отдельную систему,
но речь сейчас не о ней. Я не использую OpenTelemetry напрямую, потому что в моей парадигме приложение не должно знать,
кто и как за ним наблюдает, оно лишь должно «выплёвывать» наружу логи и метрики, а уж кто их подберёт, не так и важно.
handlers: каталог, где лежат хэндлеры, т.е. обработчики событий от Telegram. Некоторые хэндлеры сгруппированы в один файл, некоторые настолько сложные, что для них выделяется отдельный py-файл. В __init__.py обычно импортирую все рядом расположенные файлы хэндлеров и кладу функцию get_routers(), которая собирает иерархию роутеров. Пример из одного из моих проектов:
from aiogram import F, Router
from . import start, errors, admin
def get_routers(
admins_list: list[int],
) -> list[Router]:
admin.router.message.filter(F.from_user.id.in_(admins_list))
return [
start.router,
errors.router,
admin.router,
]
Аналогичным образом устроены каталоги filters и middlewares, если в проекте нужны фильтры и мидлвари соответственно.
i18n: каталог для разных локалей (i18n – сокращение от internationalization). Тут лежат переводы различных строк бота
на разные языки и обёртка для работы со строками через Mozilla Fluent.
settings.toml и settings.example.toml: файл с реальными («продовыми») настройками и файл с заглушками. Первый всегда лежит в .gitignore и никогда не покидает пределы компьютера, на котором был создан, второй обязательно коммитится в git. Пример такого файла-шаблона из одного из моих проектов:
[bot]
token = "1234567890:AaBbCcDdEeFfGrOoShALlMmNnOoPpQqRrSs"
max_quote_len = 2000
admins = [1234567]
[logs]
project_name = "some_bot"
show_datetime = true
datetime_format = "%Y-%m-%d %H:%M:%S"
show_debug_logs = true
time_in_utc = false
use_colors_in_console = false
renderer = "json"
allow_third_party_logs = true
__main__.py: точка входа. Тут читаются все конфигурационные данные, настраивается бот, подключения к базам данных,
инициализация прочих коннекторов и запускается получение данных поллингом или настраивается вебхук.
А бот легко запускается как uv run -m bot (без uv это было бы python -m bot в активированном Venv).
Не пугайтесь такого сложного описания, в реальности всё выглядит довольно просто, а какие-то куски кода можно спокойно в неизменном виде копировать из проекта в проект. Сейчас подготовим каркас вашего первого бота.
В каталоге, где вы инициализировали ранее uv-проект, создайте следующую структуру с пустыми файлами (не трогайте pyproject.toml и uv.lock):
bot
├── __init__.py
├── __main__.py
├── config.py
├── handlers
│ ├── __init__.py
│ └── start.py
└──logging_config.py
settings.example.toml
settings.toml
pyproject.toml
uv.lock
Дополнительно установите пару библиотек, которые пригодятся для работы с конфигурацией и логами:
uv add pydantic-settings structlog
Если каталог является частью git-репозитория, то обязательно добавьте settings.toml в .gitignore. Содержимое файлов
config.py и logging_config.py возьмите из каталога
01_quickstart из git-репозитория
к этой книге. Далее поговорим уже непосредственно о бизнес-логике.
Второй бот¶
Поставим цель на эту главу: написать бота, который будет на команду /start отвечать «Привет!»,
а на любой другой ввод отвечать «Я тебя не понимаю». Начнём с более широкого, т.е. с обработчика «любого сообщения».
Когда диспетчер получает событие от Telegram, перед ним встаёт задача выбрать, какой из зарегистрированных хэндлеров будет заниматься его обработкой. Для этого диспетчер по очереди проверяет каждый хэндлер на соответствие некоторому признаку – фильтру. Если условие в фильтре возвращает True, то диспетчер передаёт событие (апдейт) хэндлеру и на этом всё. Если фильтр вернул False, то диспетчер пробует следующий хэндлер и так далее, пока обработчики не закончатся. Делаем следующие важные выводы:
• Хэндлер обеспечивает обработку события от Telegram.
• Хэндлер должен быть зарегистрирован в диспетчере.
• Диспетчер выбирает хэндлеры по очереди, проверяя фильтры каждого хэндлера.
• Диспетчер передаёт апдейт первому хэндлеру, чей фильтр вернул True. Если таких нет, то апдейт не отдаётся никому.
Поскольку диспетчер существует в единственном экземпляре, тащить его из файла в файл для регистрации хэндлеров
может быть затруднительно и легко приводит к циклическим зависимостям: file_1 импортирует код из file_2 для регистрации хэндлера, в file_2 импортируется диспетчер из file_1 -> циклический импорт, ошибка. Для решения этой проблемы существуют роутеры. Роутер – это объект, к которому регистрируются хэндлеры, а дальше роутер регистрируется у диспетчера. Более того, можно поставить фильтр не только на хэндлер, но и на роутер, тогда, если апдейт не прошёл фильтр роутера, то ни один хэндлер роутера проверяться не будет. Из этого делаем ещё несколько выводов:
• Хэндлеры привязываются к диспетчеру явно (напрямую) или неявно (через роутер).
• Роутер объединяет в себе один или несколько хэндлеров.
• Диспетчер – это корневой роутер.
• Фильтры можно ставить как на хэндлеры, так и на роутер целиком.
Более подробно фильтры, а также ещё одна не описанная выше сущность – мидлвари – будут рассматриваться в следующих главах.
Таким образом, при проектировании бизнес-логики задача программиста – правильно организовать иерархию хэндлеров, чтобы для обработки апдейта выбирался именно желаемый хэндлер, а не какой-либо другой.
Скопируйте в файл bot/handlers/start.py следующий код:
from aiogram import Router # [1]
from aiogram.types import Message # [1]
router = Router(name="start") # [2]
@router.message() # [3]
async def any_message(
message: Message,
) -> None:
await message.answer("Я тебя не понимаю")
Перед вами очень простой роутер с одним хэндлером. Цифрами выше обозначены:
- Импорты. Первый (
Router) обязателен, второй (Message) нужен только для типизации и подсказок в IDE. - Создание экземпляра роутера. Необязательный параметр
nameзадаёт имя роутера, но это ни на что не влияет. - Декоратор теперь навешивается не на диспетчер, а на роутер.
По ТЗ у нас должно быть два хэндлера, давайте теперь добавим второй. В этот раз можно всё положить в один роутер, поскольку код небольшой и оба обработчика в целом похожи. Но где разместить хэндер – выше или ниже функции any_message? Сейчас у существующего хэндлера нет никаких фильтров, кроме того, что хэндлер срабатывает на апдейты типа Message. Поскольку наш будущий хэндлер на команду /start более узкоспециализированный, имеет смысл его добавить выше по коду, чтобы он был зарегистрирован раньше. В таком случае сначала диспетчер/роутер проверит хэндлер на соотвествие фильтру «это команда /start», а если нет, то проверит следующий фильтр «любое сообщение», который всегда вернёт True.
Замените полностью содержимое файла start.py следующим содержимым:
from aiogram import Router
from aiogram.filters import CommandStart # [1]
from aiogram.types import Message
router = Router(name="start")
@router.message(CommandStart()) # [2]
async def cmd_start(
message: Message,
) -> None:
await message.answer("Привет!")
@router.message() # [3]
async def any_message(
message: Message,
) -> None:
await message.answer("Я тебя не понимаю")
Обратим внимание на следующие моменты:
- Добавлен импорт фильтра
CommandStart. Он, как видно из названия, вернёт True, если сообщение является командой/start. Для любых других команд есть общий фильтрCommand, вы его увидите в следующих главах. - Вышеупомянутый фильтр вешается на хэндлер
cmd_start(). - Старый хэндлер остаётся нетронутым: нет нужды для
any_message()создавать отдельный фильтр «всё, что не является командой/start», поскольку при срабатывании хэндлераcmd_start()другие хэндлеры проверяться не будут.
Небольшое отступление: изредка бывают ситуации, когда фильтр является динамическим, т.е. конкретные его
параметры неизвестны до момента запуска самого бота. В таком случае регистрировать хэндлеры надо не через декоратор,
а через метод register() роутера или диспетчера. Тогда бы файл start.py выглядел чуть иначе:
from aiogram import Router
from aiogram.filters import CommandStart
from aiogram.types import Message
router = Router(name="start")
async def cmd_start(
message: Message,
) -> None:
await message.answer("Привет!")
async def any_message(
message: Message,
) -> None:
await message.answer("Я тебя не понимаю")
# Где-то ниже в этом файле, возможно, внутри какой-то функции:
router.message.register(cmd_start, CommandStart())
router.message.register(any_message)
Роутер создан, хэндлеры тоже. Хэндлеры привязаны к роутеру, но сам роутер пока ни к чему не привязан.
Давайте для начала соберём иерархию роутеров (он у нас один, но всё же). Для этого в bot/handlers/__init__.py
впишите следующий код:
from aiogram import Router
from . import (
start,
)
def get_routers() -> list[Router]:
return [
start.router,
]
Здесь всё просто: импортируем все файлы с хэндлерами из bot/handlers и возвращаем список роутеров в нужном порядке,
именно в таком порядке их будет проверять диспетчер. А что же сам диспетчер? Его создадим в bot/__main__.py вместе со всеми другими объектами и запуском бота:
import asyncio
import structlog
from structlog.typing import FilteringBoundLogger
from aiogram import Bot, Dispatcher
from bot.config import Settings
from bot.handlers import get_routers
from bot.logging_config import get_structlog_config
logger: FilteringBoundLogger = structlog.get_logger()
async def main() -> None:
# Чтение конфигурации (toml-файл или env vars – не важно)
settings = Settings()
# Конфигурирование логгера
structlog.configure(**get_structlog_config(settings.logs))
# Создание объекта бота. Обязательный аргумент token – читаем токен
# из конфигурации. Поскольку токен помечен как SecretStr, то необходимо
# дополнительно вызывать get_secret_value().
bot = Bot(
token=settings.bot.token.get_secret_value(),
)
# Создание объекта диспетчера и привязка роутеров
dp = Dispatcher()
# Небольшой лайфхак: include_routers() принимает на вход
# произвольное количество аргументов
# get_routers() возвращает список: [A, B, C,...]
# и этот список будет передан как набор аргументов:
# include_routers(A, B, C,...)
dp.include_routers(*get_routers())
# Запуск бота в режиме поллинга
await logger.ainfo("Starting polling...")
try:
await dp.start_polling(bot)
finally:
await logger.ainfo("Bot stopped")
asyncio.run(main())
Выше проставлены комментарии прямо в коде, отдельно на них останавливаться сейчас не будем. Обновите файл settings.example.toml:
[bot]
token = "1234567890:AaBbCcDdEeFfGrOoShAHhIiJjKkLlMmNnOo"
[logs]
project_name = "hello_world"
show_datetime = true
datetime_format = "%Y-%m-%d %H:%M:%S"
show_debug_logs = false
time_in_utc = false
use_colors_in_console = true
renderer = "console" # "console" or "json"
allow_third_party_logs = true
А в settings.toml скопируйте код выше, но замените токен на ваш реальный, полученный у @BotFather. И теперь смело запускайте бота:
$ uv run -m bot
2026-05-07 14:38:49 [info] Starting polling... project_name=hello_world
2026-05-07 14:38:49 [info] Start polling project_name=hello_world
2026-05-07 14:38:49 [info] Run polling for bot @bot id=1234567890 - 'test_bot' project_name=hello_world
Напишите что-нибудь боту, чтобы убедиться, что он работает корректно:

И снова поздравляю! Вы написали второго бота на aiogram и заложили фундамент, на основе которого в следующих главах будем рассматривать другие возможности фреймворка и Telegram Bot API.