Aiogram 3: основы создания Telegram-бота
Если вы искали, с чего начать писать Telegram-бота на Python в 2026 году, ответ почти наверняка — Aiogram 3. Библиотека полностью асинхронная, разработчики активно её поддерживают, а старые примеры под Aiogram 2 из интернета работать не будут — синтаксис поменялся заметно. Дальше — рабочий минимум: от установки до бота, который переживёт перезагрузку сервера.
Содержание
Чем Aiogram 3 отличается от Aiogram 2
Разница не косметическая, это по сути другой фреймворк с похожим названием.
Router вместо Dispatcher как держателя хендлеров. В Aiogram 2 весь код регистрировался в одном Dispatcher через декораторы @dp.message_handler(...). В Aiogram 3 хендлеры вешаются на объект Router, а роутеры потом подключаются к Dispatcher через dp.include_router(). Это позволяет разбить бота на модули: один роутер на регистрацию, другой на админку, третий на платежи — и каждый живёт в своём файле, не завися от глобального объекта диспетчера.
FSM (машина состояний) переехала в middleware. В Aiogram 2 состояния хранились через state_storage и подключались к диспетчеру напрямую. В третьей версии FSM работает как встроенный middleware поверх Dispatcher, а хранилище состояний (MemoryStorage, RedisStorage) передаётся при создании диспетчера. Логика та же — состояния и переходы между ними, но код чище.
Явные фильтры вместо метода filters=[...]. В Aiogram 3 фильтрация построена вокруг magic-фильтра F (аналог field из aiogram.types) и класса Filter. Синтаксис F.text == "привет" читается почти как обычный Python и не требует лямбд.
Полная типизация и Pydantic. Все объекты API (Message, CallbackQuery, User) — типизированные Pydantic-модели. IDE подсказывает поля, автодополнение работает без сюрпризов.
Требования к окружению. Aiogram 3 формально работает от Python 3.8, но на практике разумно ставить 3.11 или новее — часть зависимостей рассчитана на актуальные релизы Python. Точную минимальную версию Python и номер текущего релиза Aiogram 3 сверяйте командой pip index versions aiogram перед установкой — библиотека обновляется, и любые зафиксированные здесь цифры быстро устареют.
Если раньше писали на Aiogram 2 и переносите старого бота — перепишите заново, а не патчите: различий в архитектуре слишком много, экономии времени не получится.
Установка и подготовка окружения
Ставить Aiogram 3 в системный Python — плохая идея, зависимости бота быстро начнут конфликтовать с другими проектами на сервере. Работаем через виртуальное окружение.
mkdir mybot && cd mybot
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install aiogram python-dotenv
python-dotenv не обязателен, но токен бота хранить в коде — плохая практика: файл рано или поздно попадёт в git или в публичный репозиторий. Создайте .env:
BOT_TOKEN=1234567890:AAExampleTokenNotReal
И добавьте .env в .gitignore, если используете git.
Токен получают у @BotFather в самом Telegram: команда /newbot, имя бота, username, оканчивающийся на bot. В ответ приходит токен вида <id>:<хэш> — он и нужен в .env.
Проверить, что всё установилось, можно так:
python -c "import aiogram; print(aiogram.__version__)"
Если версия начинается на 3. — окружение готово.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSМинимальный рабочий бот
Самый простой рабочий бот — один файл, один хендлер на команду /start и один на эхо любого текста.
# main.py
import asyncio
import logging
import os
from aiogram import Bot, Dispatcher, Router
from aiogram.filters import CommandStart
from aiogram.types import Message
from dotenv import load_dotenv
load_dotenv()
logging.basicConfig(level=logging.INFO)
router = Router()
@router.message(CommandStart())
async def cmd_start(message: Message) -> None:
await message.answer(f"Привет, {message.from_user.full_name}!")
@router.message()
async def echo(message: Message) -> None:
await message.answer(message.text or "Это не текст, я умею отвечать только на текст")
async def main() -> None:
bot = Bot(token=os.environ["BOT_TOKEN"])
dp = Dispatcher()
dp.include_router(router)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Запуск:
python main.py
Обратите внимание на порядок хендлеров в роутере: @router.message() без фильтров ловит вообще всё, поэтому его нужно регистрировать последним — иначе он перехватит апдейты раньше специфичных хендлеров вроде cmd_start.
Здесь используется start_polling — бот сам опрашивает Telegram API каждые несколько секунд. Это самый простой способ запуска и для старта на VPS его достаточно; про альтернативу через вебхуки и когда она оправдана — в статье про webhook и polling.
Фильтры: как отбирать нужные апдейты
Фильтры — это условия, по которым хендлер решает, брать сообщение на обработку или нет.
Встроенные фильтры для команд:
from aiogram.filters import Command, CommandStart
@router.message(CommandStart())
async def start(message: Message): ...
@router.message(Command("help"))
async def help_cmd(message: Message): ...
@router.message(Command("price", "cost")) # несколько вариантов сразу
async def price(message: Message): ...
Magic-фильтр F — самый гибкий инструмент, работает как обёртка над полями объекта:
from aiogram import F
@router.message(F.text.contains("сервер"))
async def about_server(message: Message):
await message.answer("Расскажу про аренду VPS")
@router.message(F.photo)
async def on_photo(message: Message):
await message.answer("Фото получил")
@router.callback_query(F.data == "confirm")
async def on_confirm(callback):
await callback.answer("Подтверждено")
Можно комбинировать через & и |:
@router.message(F.text & F.chat.type == "private")
async def private_text(message: Message): ...
Свои фильтры пишутся как класс, наследующий Filter, если условие сложнее одной строки — например, проверка роли пользователя в базе. Для простого бота обычно хватает Command и F.
FSM: состояния для многошаговых сценариев
Когда бот должен последовательно спросить несколько вещей (например, имя, потом телефон, потом подтверждение) — используется FSM (Finite State Machine).
from aiogram.filters import StateFilter
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.storage.memory import MemoryStorage
class OrderForm(StatesGroup):
name = State()
phone = State()
@router.message(Command("order"))
async def start_order(message: Message, state: FSMContext):
await state.set_state(OrderForm.name)
await message.answer("Как вас зовут?")
@router.message(StateFilter(OrderForm.name))
async def process_name(message: Message, state: FSMContext):
await state.update_data(name=message.text)
await state.set_state(OrderForm.phone)
await message.answer("Телефон для связи?")
@router.message(StateFilter(OrderForm.phone))
async def process_phone(message: Message, state: FSMContext):
data = await state.update_data(phone=message.text)
await state.clear()
await message.answer(f"Записал: {data['name']}, {data['phone']}")
Хранилище состояний передаётся при создании диспетчера:
dp = Dispatcher(storage=MemoryStorage())
MemoryStorage держит состояния в оперативной памяти процесса — просто, но при перезапуске бота все незавершённые сценарии теряются. Для продакшена, где бот может перезапускаться (обновление кода, systemd-рестарт), логичнее RedisStorage — состояния переживут рестарт процесса, если Redis работает отдельно. Это требует поднятого Redis на сервере — как его развернуть, описано в статье про установку Redis на VPS.
Структура проекта для бота посерьёзнее
Один файл main.py нормален для прототипа, но как только хендлеров становится десяток-другой, лучше разложить код по модулям.
mybot/
├── venv/
├── .env
├── main.py
├── config.py
├── handlers/
│ ├── __init__.py
│ ├── start.py
│ ├── order.py
│ └── admin.py
├── keyboards/
│ ├── __init__.py
│ └── main_menu.py
├── middlewares/
│ └── throttling.py
└── requirements.txt
config.py — чтение переменных окружения в одном месте:
import os
from dataclasses import dataclass
from dotenv import load_dotenv
load_dotenv()
@dataclass
class Config:
bot_token: str
admin_id: int
def load_config() -> Config:
return Config(
bot_token=os.environ["BOT_TOKEN"],
admin_id=int(os.environ["ADMIN_ID"]),
)
Каждый модуль в handlers/ заводит свой Router и экспортирует его:
# handlers/start.py
from aiogram import Router
from aiogram.filters import CommandStart
from aiogram.types import Message
router = Router(name="start")
@router.message(CommandStart())
async def cmd_start(message: Message) -> None:
await message.answer("Привет! Это стартовый роутер.")
А main.py только собирает всё вместе:
import asyncio
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from config import load_config
from handlers import start, order, admin
async def main() -> None:
config = load_config()
bot = Bot(token=config.bot_token)
dp = Dispatcher(storage=MemoryStorage())
dp.include_router(start.router)
dp.include_router(order.router)
dp.include_router(admin.router)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Такая структура держится годами без переписывания — новый функционал добавляется новым файлом в handlers/, а не правкой одного разросшегося main.py.
Запуск как systemd-сервис на VPS
python main.py в терминале умирает вместе с сессией SSH. Для постоянной работы бот нужно оформить как systemd-сервис — тогда он переживёт разрыв соединения, перезапустится сам после сбоя и поднимется автоматически после перезагрузки сервера.
Сначала зафиксируйте зависимости:
pip freeze > requirements.txt
Создайте юнит-файл:
sudo nano /etc/systemd/system/mybot.service
[Unit]
Description=Telegram bot on aiogram 3
After=network.target
[Service]
Type=simple
User=botuser
WorkingDirectory=/home/botuser/mybot
ExecStart=/home/botuser/mybot/venv/bin/python /home/botuser/mybot/main.py
Restart=on-failure
RestartSec=5
EnvironmentFile=/home/botuser/mybot/.env
[Install]
WantedBy=multi-user.target
Запускать бота от отдельного непривилегированного пользователя (botuser), а не от root — стандартная гигиена для любого сервиса на сервере.
sudo systemctl daemon-reload
sudo systemctl enable mybot
sudo systemctl start mybot
sudo systemctl status mybot
Логи смотрите через:
journalctl -u mybot -f
После правки кода не забывайте sudo systemctl restart mybot — иначе процесс продолжит работать со старой версией файла в памяти. Если бот падает сразу после старта и рестартует по кругу, разбор типичных причин есть в статье про бота, который падает после перезапуска.
Для polling важна не столько мощность сервера, сколько его непрерывная доступность. Сколько ресурсов реально нужно под такого бота — отдельная тема, разобрана в статье сколько RAM нужно для Telegram-бота.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Aiogram 3 совместим с кодом на Aiogram 2?
Нет, API изменился достаточно сильно (Router, синтаксис фильтров, работа с FSM), старый код нужно переписывать, а не адаптировать точечно.
Обязательно ли использовать FSM для простого бота?
Нет. Если у бота нет многошаговых диалогов — достаточно хендлеров с фильтрами Command и F, FSM усложнит код без пользы.
Что выбрать для хранения FSM-состояний в продакшене — Memory или Redis?
MemoryStorage подходит для разработки и небольших ботов без критичных сценариев. Если бот перезапускается автоматически (например, по расписанию или после деплоя) и важно не терять незавершённые диалоги пользователей — берите RedisStorage.
Можно ли запускать несколько ботов на одном VPS?
Да, каждый бот — отдельный процесс со своим systemd-юнитом и своим виртуальным окружением; они не мешают друг другу, если не конкурируют за одни и те же порты (актуально только для вебхуков).
Polling или webhook — с чего начинать?
Начинайте с polling — он проще в настройке и не требует домена с SSL. Переходить на вебхуки стоит, когда важна задержка ответа или бот обслуживает заметный поток сообщений.