К каналу

selfrotgram · Telegram Bot API для Python

Типы говорят правду

Если хендлер обещает, что у сообщения есть текст, фильтр это проверил, а message.text в редакторе — str, а не str | None. Без assert, без cast, без скрытых аргументов.

Альфа: 0.1.1–0.1.4 на PyPI, сентябрь 2026. Работает на реальном боте — chestor_bot.

echo_bot.py
class Echo(MessageHandler[BaseContext[TextMessage]]):  # обещаю: текст есть
    query = HasText()  # проверяю: текст есть

    async def handle(self) -> None:
        # text: str — проверять нечего
        await self.ctx.message.answer(self.ctx.message.text)

$ python -m bot Запущен @echo_bot, жду апдейты (long polling)

01 · Что гарантирует фильтр

Обещание, проверка, сверка

Нажми «Забыть HasText()» в примере выше: библиотека не даст запуститься боту, который врёт о своих типах.

Фильтр гарантирует

HasText() пропускает только сообщения с текстом. Фото без подписи до handle() не дойдёт.

Заголовок обещает

TextMessage в MessageHandler[…] — суженный тип: у него text: str. Таких типов 117, по одному на каждое необязательное поле.

Библиотека сверяет

Обещание и фильтр сравниваются при импорте. Разошлись — DefinitionError при запуске, а не AttributeError у пользователя.

02 · Было / стало

Если ты писал на aiogram

Главное отличие в подходе: aiogram опирается на функции и неявную передачу аргументов, selfrotgram — на классы и явный ctx.

было · aiogram 3
@router.message(Command("sum"))
async def sum_handler(message: Message, command: CommandObject):
    if not command.args or len(command.args.split()) != 2:
        await message.answer("Использование: /sum 2 3")
        return
    a, b = map(int, command.args.split())
    await message.answer(f"{a + b}")
стало · selfrotgram
class Sum(MessageHandler[BaseContext[TextMessage]]):
    cmd = Command("sum", args_count=2)
    query = cmd

    async def handle(self):
        a, b = (int(x) for x in self.cmd.parse(self.ctx).args)
        await self.ctx.message.answer(f"{a + b}")

Команда с другим числом аргументов просто не подходит — проверять руками нечего.

Все пары и честный список отличий — в docs/from-aiogram.md.

03 · CLI

selfrot tree и selfrot check

tree рисует дерево роутеров и находит недостижимые хендлеры. check собирает все ошибки описания разом — для CI, с --strict.

$ 

$ selfrot tree Dispatcher (мидлвари: LoggingMiddleware) └─ RootRouter ├─ StartRouter │ └─ Start message: TextMessage Command('start') └─ AdminRouter (мидлвари: OnlyAdmins) ├─ BanUser message: TextMessage (FromUser(1, 2) & Command('ban', Ban)) ├─ Anything message без фильтра: ловит всё этого вида ├─ Never message: TextMessage HasText() ! недостижим: выше Anything ... └─ Joined chat_member: ChatMemberUpdated MemberJoined() Роутеров: 3 (без корня), хендлеров: 4. allowed_updates: chat_member, message

04 · Внутри

Генерируется из спецификации

версия Bot API
10.3
типов из спецификации
400
методов
185
суженных типов
117
видов хендлеров
26
тестов
200+

Что есть

  • Типы, методы и фильтры генерируются из официальной спецификации: вышел новый Bot API — перегенерировали.
  • Всё, что нужно хендлеру, лежит в self.ctx. Нет аргументов, появляющихся по имени, нет прокси F.
  • CommandArgs, AnyCommand, Reply[T], отложенные вызовы defer и after_handle, вебхуки, скачивание файлов.
  • Типизированные FSM-диалоги и callback_data, pressed_by — «нажать может только владелец».
  • Лимиты Telegram проверяются до отправки: 64 байта callback_data, 8 кнопок в ряду.
  • CI на Python 3.11–3.13: тесты, pyright, сверка сгенерированного кода.

Чего пока нет

  • Локализации и сцен.
  • Готового хранилища FSM в Redis — есть интерфейс Storage из трёх методов.
  • Загрузки файлов внутри альбомов и сборщика reply-клавиатур.
  • Экосистемы и обкатки тысячами ботов — aiogram старше и больше, это честно.

05 · Установка

Поставить и попробовать

Ставится как selfrotgram, импортируется как selfrot. Зависимости совместимы с aiogram 3.x — переезжать можно постепенно.

  • $ pip install selfrotgram
  • $ poetry add selfrotgram
  • $ selfrot init