AiogramX
Why I Built It
- Production bots I maintain (especially Tahrirchi) kept re‑implementing the same inline UIs—paginators, calendars, time pickers—with slightly different edge cases every time.
- Most third-party widget libraries register one callback handler per instance; after a busy day the router list explodes and callbacks slow down.
- AiogramX packages the components I kept rewriting into a reusable toolkit while leaning on
flipcache’s LRU containers so widgets stay fast no matter how many users are hammering them.
Event Flow
- Bot routers register widgets once via
WidgetBase.register(dp); each widget type wires a single callback entry point. - When a user interacts with an inline button, Aiogram parses callback data into a
_cbmodel that always contains a short-livedkey. - The
WidgetBaselookup pulls the instance from anLRUDict(max_items=1000)so stale widgets fall out automatically. - The widget-specific
process_cbimplementation updates state, re-renders the keyboard, and emits domain-friendly responses (e.g.date,time,CheckboxResult).
Widget Suite
| Component | What it solves | Highlights |
|---|---|---|
Paginator | Browsing large datasets lazily | Supports eager lists, async loaders, row/page sizing and per-page callbacks |
Calendar | Date selection with guard rails | Localised quick-jump buttons, max-range enforcement, expiring widgets |
Checkbox | Collecting multi-select input | Supplies structured dict payloads and back button hooks |
TimeSelectorGrid / TimeSelectorModern | Time picking UX | Enforces allowed windows, 5 min offsets, custom labels, carry-over logic |
ReplyKeyboardMeta | Static reply menus | Define layouts via class attributes, produces ready-to-use ReplyKeyboardMarkup |
Implementation Highlights
WidgetMetaensures every widget exposes a_cbCallbackDataclass with akeyfield; missing pieces raise at import time instead of failing in production.WidgetBaseseeds a class-levelLRUDict(from FlipCache) so only the most recent ~1000 widget instances stay resident, keeping callback lookups O(1).watchdogstyle “expired” responses are localised (en,ru,uz) viaget_expired_text, and stale keyboards auto-clear to avoid dangling inline buttons.- Utilities such as
gen_keyguarantee key uniqueness across thousands of concurrent widget instances without hitting Redis or Postgres. - The project is packaged as
aiogramx==3.1.3(Python ≥3.9), ships typed hints, and re-exports the widget classes from the package root for frictionless imports.
Release Notes Snapshots
- Jun 2025 · v3.1.3 — Added
ReplyKeyboardMeta, carry-over logic for time selectors, and automated PyPI publishing via CI. - May 2025 · v3.1.2 — Fixed stray
awaits, relaxed type hints for localisation, and introduced a live demo bot. - May 2025 · v3.1.0 — Landed
Calendar+Checkboxsuites, refactored the widget core, and renamed the time selector API.
Example
Python
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import CallbackQuery, Message
from aiogramx import Paginator
router = Router()
Paginator.register(router)
async def fetch_rows(page: int, per_page: int):
rows = await repo.fetch_batch(page=page, size=per_page)
return [row.as_button() for row in rows]
@router.message(Command("browse"))
async def browse_handler(message: Message):
paginator = Paginator(
per_page=12,
per_row=3,
lazy_data=fetch_rows,
on_select=lambda cq, payload: cq.answer(f"Picked {payload}"),
)
await message.answer("Choose an item", reply_markup=await paginator.render_kb())
@router.callback_query(F.data.startswith("item:"))
async def legacy_handler(callback: CallbackQuery):
await callback.answer("Legacy callbacks still coexist with AiogramX")
What’s Next
- Expand localisation bundles for widgets (currently
en,ru,uz) and auto-detect right-to-left layouts. - Introduce a form builder that chains multiple widgets into a single conversation with built-in validation.
- Publish typed stub files so editors can auto-complete callback payload structures out-of-the-box.

