A powerful builder library for creating recursive inline keyboard menus in python-telegram-bot
Project description
Telegram Menu Builder
A type-safe, async-first Python library for building recursive inline keyboard menus in
python-telegram-bot v20+. You declare
buttons with a fluent MenuBuilder, the library encodes each callback payload to fit Telegram's
64-byte limit (compressing inline or spilling to pluggable storage), and a MenuRouter decodes
incoming callbacks and dispatches them to your handlers.
📖 Documentation: https://smoxy.github.io/telegram-menu-builder/ — or jump to the doc index below.
Status: alpha (
0.x). The API may change before1.0. See the changelog.
✨ Features
- 🏗️ Fluent builder API — chainable, readable menu construction.
- 📦 Smart callback encoding — automatic inline / short-term / persistent strategy to stay under Telegram's 64-byte limit, with zlib compression and deduplication.
- 🧭 Routing & middleware —
MenuRouterdispatches callbacks to named handlers withbefore/after/on_errorhooks and handler groups. - 🔄 Unlimited nesting — submenus and navigation (back / next / exit / cancel) buttons.
- 🧩 Pluggable storage — built-in in-memory, async SQL, and Redis/Valkey backends are included;
or implement the
StorageBackendprotocol (or subclassBaseStorage) for your own. - 🪄 Application-free output —
to_markup()/to_raw()build atelegram.InlineKeyboardMarkupor a plain Bot API dict synchronously, with no PTBApplication, event loop, or storage required — share one menu definition across codebases. - 🔐 Strict typing — full type hints, validated with both
mypy --strictandpyright(Pydantic v2 models), shipped withpy.typed. - 🧪 Well tested — ~90% coverage, CI on every push and pull request, plus a
telegram_menu_builder.testingmodule to drive handlers with no live Telegram.
🚀 Quick start
pip install telegram-menu-builder
Optional extras: telegram-menu-builder[redis] (built-in Redis/Valkey backend), [sql] (plus
[postgres] / [mysql] drivers), [dev], [docs]. See Installation.
from telegram import Update
from telegram.ext import Application, CallbackQueryHandler, ContextTypes
from telegram_menu_builder import MenuBuilder, MenuRouter
router = MenuRouter()
async def show_settings(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
menu = (
MenuBuilder()
.add_item("🌍 Language", handler="set_language")
.add_item("👤 Profile", handler="edit_profile", user_id=update.effective_user.id)
.columns(2)
.add_back_button()
.build()
)
await update.message.reply_text("⚙️ Settings", reply_markup=menu)
@router.handler("set_language")
async def handle_language(update: Update, context: ContextTypes.DEFAULT_TYPE, params: dict) -> None:
# `params` are the values you passed to add_item(...), decoded from the callback.
await update.callback_query.edit_message_text("Choose a language…")
app = Application.builder().token("YOUR_TOKEN").build()
app.add_handler(CallbackQueryHandler(router.route))
app.run_polling()
Tip: in
asynccode, preferawait builder.build_async().build()is a synchronous convenience wrapper (it also works inside a running event loop). See Building menus.
📚 Examples at a glance
Buttons carry arbitrary parameters — they are encoded into the callback and decoded back into
the params dict your handler receives:
menu = (
MenuBuilder()
.add_item("📝 Edit", handler="edit_user", user_id=123, field="email")
.add_item("🗑️ Delete", handler="delete_user", user_id=123, confirm=True)
.columns(1)
.add_back_button(handler="user_list", page=2)
.build()
)
Nested submenus:
users = MenuBuilder().add_item("Add user", handler="add_user").add_back_button()
main = MenuBuilder().add_submenu("👥 Users", users)
Storage backends — in-memory, SQL, and Redis/Valkey backends ship built-in; for anything else,
subclass BaseStorage (or satisfy the StorageBackend protocol) and pass it in:
from telegram_menu_builder import MenuBuilder
from telegram_menu_builder.storage import RedisStorage # one client, also speaks Valkey
builder = MenuBuilder(storage=RedisStorage(url="redis://localhost:6379/0"))
# custom backends: docs/advanced/custom-storage.md
See the runnable examples/ and the guides for complete, copy-pasteable bots — including ConversationHandler integration.
🧠 How callback encoding works
Telegram limits callback_data to 64 bytes. The library chooses a strategy automatically:
- Inline (fits in 64 bytes) — JSON → zlib → base64 directly in the callback.
- Short-term — stored in the backend with a TTL; the callback carries a short reference.
- Persistent — stored without expiry for large/long-lived payloads.
Identical payloads reuse the same key (deduplication). Full details: Callback encoding internals.
📖 Documentation
📚 Rendered site: https://smoxy.github.io/telegram-menu-builder/
| Get started | Guides | Advanced | Reference |
|---|---|---|---|
| Installation | Building menus | Encoding internals | MenuBuilder |
| Quick start | Routing callbacks | Custom storage | MenuRouter |
| Storage backends | Types & models | ||
| ConversationHandler | Encoding · Storage |
Project docs: Changelog · Security policy · Dependency & CVE audit · Python compatibility · Development guide · Contributing
🤝 Contributing
Every contribution is welcome — bug reports, documentation, examples, and code alike. Start with the Contributing guide and the Development guide.
git clone https://github.com/smoxy/telegram-menu-builder.git
cd telegram-menu-builder
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
pre-commit install
make test # run the suite (≈90% coverage)
Before opening a pull request, make sure the gates pass:
make lint # ruff + black --check
make type-check # mypy --strict + pyright
make test # pytest with coverage
- 🐛 Bugs → open an issue (templates provided).
- 💡 Ideas / questions → GitHub Discussions.
- 🤖 AI-assisted development → this repo ships
CLAUDE.md,AGENTS.md, and ready-made agents/skills under.claude/to keep changes consistent.
Releases are published to PyPI automatically via GitHub Releases and trusted publishing — no manual token handling needed.
🔐 Security
Found a vulnerability? Please report it privately — see SECURITY.md. Dependency CVEs
are tracked in the dependency audit (for example, pydantic is pinned
>=2.4 to exclude CVE-2024-3772).
🗺️ Roadmap
- ✅ Fluent builder, navigation, submenus
- ✅ Smart callback encoding (inline / short-term / persistent) with compression & dedup
- ✅
MenuRouterwith middleware and handler groups - ✅ In-memory storage backend, strict typing, CI
- ✅ Built-in SQL backend via SQLAlchemy (async) for PostgreSQL/Supabase, MySQL/MariaDB, and SQLite
(install
[sql], plus[postgres]or[mysql]for those drivers — see storage backends) - ✅ Built-in Redis backend via
redis-pythat also speaks Valkey (the RESP-compatible Redis fork we recommend), verified against Redis 7.4.9 & Valkey 8.1.8 — install[redis](see Redis/Valkey storage) - ✅ Application-free build (
to_markup/to_raw) + atomic multi-operator claim + testing helpers (v0.4.0) - 📅 Helpers for pagination and form/wizard flows
📝 License
Released under the MIT License — see LICENSE. Free for commercial, private, and personal use, modification, and distribution. Using this code to train AI/ML models is explicitly permitted. Attribution is appreciated but not required.
Made with ❤️ for the Telegram Bot community · python-telegram-bot v20+ · Pydantic v2
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters