Library-agnostic universal inline keyboard builder for python Telegram bots APIs
Project description
Inline Keyboard Builder (v3) — Python
Universal inline keyboard builder for Telegram bots. Produces pure Telegram Bot API compliant JSON, usable with any library (python-telegram-bot, Aiogram, Pyrogram, Telebot…).
Version 3 brings full feature parity with the JavaScript/TypeScript edition:
add_callback_button_from_parts(),callback_data_parse(),preview(),paginated_list(), and strongly typed interfaces throughout.
Table of Contents
- Installation
- Import
- Package structure
- Core concept
- Public API
- Usage examples
- Premium button styles
- Payment buttons
- Common errors
- Migration to v3
- Support this project
- Contribution
Installation
pip install telegram-inline-keyboard-builder
Import
from telegram_inline_keyboard_builder import InlineKeyboardBuilder
# Optional: import type hints for your own type-annotated code
from telegram_inline_keyboard_builder.types import (
PaginatedListOptions,
PaginationConfig,
PaginationLabels,
ButtonConfig,
)
Package structure
telegram_inline_keyboard_builder/
├── __init__.py # public exports
├── builder.py # InlineKeyboardBuilder class
└── types/
├── __init__.py
├── buttons.py # ButtonStyle, CallbackButton, UrlButton …
└── utils.py # PaginationLabels, PaginationConfig, PaginatedListOptions
Core concept
Telegram inline keyboards follow one universal schema.
This builder generates the keyboard directly in Telegram format and lets
you pass the result to any Telegram library — no adapters, no wrappers,
no framework coupling.
keyboard = builder.build()
# Always returns:
# { "reply_markup": { "inline_keyboard": [[...], [...]] } }
Public API
Constructor
InlineKeyboardBuilder(buttons_per_row: int = 2, auto_wrap_max_chars: int = 0)
| Parameter | Type | Default | Description |
|---|---|---|---|
buttons_per_row |
int |
2 |
Maximum buttons per row. Minimum 1. |
auto_wrap_max_chars |
int |
0 |
Auto line-break when a row exceeds this char count. 0 = disabled. |
add_callback_button()
Adds an inline button that triggers a callback query.
builder.add_callback_button(
text: str,
callback_data: str,
*,
style: Literal["primary", "danger", "success"] | None = None,
icon_custom_emoji_id: str | None = None,
) -> InlineKeyboardBuilder
builder.add_callback_button("✅ Confirm", "confirm:action")
builder.add_callback_button("🗑 Delete", "delete:42", style="danger")
add_callback_button_from_parts()
Builds a structured callback_data from scope, action, and id — no
manual string concatenation.
builder.add_callback_button_from_parts(
scope: str,
action: str,
id: str | int,
text: str,
*,
style: str | None = None,
icon_custom_emoji_id: str | None = None,
separator: str = ":",
) -> InlineKeyboardBuilder
builder.add_callback_button_from_parts("post", "like", 101, "👍 Like", style="success")
# callback_data → "post:like:101"
add_url_button()
Adds an inline button that opens an external URL.
builder.add_url_button(
text: str,
url: str,
*,
style: str | None = None,
icon_custom_emoji_id: str | None = None,
) -> InlineKeyboardBuilder
builder.add_url_button("📖 Documentation", "https://example.com")
add_pay_button()
Adds a Telegram payment button. Must only be used inside send_invoice.
builder.add_pay_button(text: str) -> InlineKeyboardBuilder
add_custom_button()
Adds a fully custom button dict for Telegram button types not covered by this
library (e.g. switch_inline_query).
builder.add_custom_button(button_object: dict) -> InlineKeyboardBuilder
builder.add_custom_button({
"text": "🔔 Share",
"switch_inline_query": "hello",
})
add_buttons()
Add multiple buttons at once from a declarative list or grouped config.
# Flat list
builder.add_buttons([
{"type": "callback", "text": "Yes", "data": "answer:yes"},
{"type": "callback", "text": "No", "data": "answer:no"},
])
# Grouped config (shared type)
builder.add_buttons({
"type": "callback",
"buttons": [
{"text": "👍", "data": "vote:up"},
{"text": "👎", "data": "vote:down"},
],
})
callback_data()
Build a callback_data string without adding a button.
data = builder.callback_data("user", "ban", 42)
# → "user:ban:42"
callback_data_parse()
Decode a callback_data string into its parts.
Useful inside callback handlers or unit tests.
builder.callback_data_parse(data: str, separator: str = ":") -> dict[str, str]
# → {"scope": str, "action": str, "id": str}
result = builder.callback_data_parse("post:like:101")
# → {"scope": "post", "action": "like", "id": "101"}
Raises ValueError if the string has fewer than three parts.
preview()
Returns a readable row-by-row representation of the keyboard — handy during development to verify layout before sending to Telegram.
print(builder.preview())
# Row 1: [👍 Like](callback:post:like:101) | [👎 Dislike](callback:post:dislike:101)
# Row 2: [📖 Docs](https://example.com)
paginated_list()
Transforms a complete list into a paginated inline keyboard with built-in navigation controls.
builder.paginated_list(options: PaginatedListOptions) -> InlineKeyboardBuilder
PaginatedListOptions
| Key | Type | Description | |
|---|---|---|---|
items |
list[T] |
required | Complete unsliced list of items. |
page |
int |
required | Current page number (starts at 1). |
per_page |
int |
required | Items displayed per page. |
render |
Callable[[T], dict] |
required | Maps one item to an InlineKeyboardButton dict. |
pagination |
PaginationConfig |
required | Navigation bar configuration. |
PaginationConfig
| Key | Type | Default | Description |
|---|---|---|---|
callback |
Callable[[int], str] |
required | Returns callback_data for a given page number. |
labels |
PaginationLabels |
— | Custom labels for navigation buttons. |
show_edge_buttons |
bool |
False |
Show ⏮ / ⏭ to jump to the first and last page. |
hide_if_single_page |
bool |
False |
Hide the navigation bar when total_pages == 1. |
counter_callback |
str |
"ignore" |
callback_data for the central counter button (e.g. 2/5). |
PaginationLabels
| Key | Default | Description |
|---|---|---|
previous |
"⬅️" |
Previous page button label. |
next |
"➡️" |
Next page button label. |
first |
"⏮" |
First page button label. |
last |
"⏭" |
Last page button label. |
Key behaviours
- Empty list — returns
selfimmediately; nothing is rendered. - Page overflow — page is silently clamped to
total_pages. - Edge navigation — on page 1 the ⬅️ button shows
·⬅️·withcallback_data="ignore". Same for ➡️ on the last page. - Validation — raises
ValueErrorifitemsis not a list orcallbackis not callable.
Layout controls
builder.set_buttons_per_row(n: int) -> InlineKeyboardBuilder
builder.set_auto_wrap_max_chars(n: int) -> InlineKeyboardBuilder
Both can be called at any point in the chain to change layout for subsequent buttons.
new_row()
Force a row break at the current position.
builder.new_row() -> InlineKeyboardBuilder
build()
Build and return the final Telegram reply_markup object.
keyboard = builder.build()
# → {"reply_markup": {"inline_keyboard": [[...], [...]]}}
Usage examples
python-telegram-bot
from telegram import Update
from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes
from telegram_inline_keyboard_builder import InlineKeyboardBuilder
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
keyboard = (
InlineKeyboardBuilder(buttons_per_row=2)
.add_callback_button("✅ Confirm", "confirm_action", style="success")
.add_url_button("🌍 Website", "https://example.com")
.new_row()
.add_callback_button("❌ Cancel", "cancel_action", style="danger")
.build()
)
await update.message.reply_text(
"Welcome 👋 Choose an option:",
reply_markup=keyboard["reply_markup"],
)
Aiogram
from aiogram import Bot, Dispatcher, Router
from aiogram.filters import CommandStart
from aiogram.types import Message, InlineKeyboardMarkup
from telegram_inline_keyboard_builder import InlineKeyboardBuilder
router = Router()
@router.message(CommandStart())
async def start(message: Message):
raw = (
InlineKeyboardBuilder(buttons_per_row=2)
.add_callback_button_from_parts("post", "like", 1, "👍 Like", style="success")
.add_callback_button_from_parts("post", "dislike", 1, "👎 Dislike", style="danger")
.new_row()
.add_url_button("📖 Docs", "https://example.com")
.build()
)
# Aiogram expects an InlineKeyboardMarkup object
markup = InlineKeyboardMarkup(inline_keyboard=raw["reply_markup"]["inline_keyboard"])
await message.answer("Choose:", reply_markup=markup)
Paginated product list
from telegram.ext import CallbackQueryHandler
from telegram_inline_keyboard_builder import InlineKeyboardBuilder
async def show_products(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
page = int(query.data.split("_")[-1]) # e.g. "products_page_2"
products = await db.get_all_products() # full list
keyboard = (
InlineKeyboardBuilder()
.paginated_list({
"items": products,
"page": page,
"per_page": 5,
"render": lambda p: {
"text": f"🛍 {p.name} — {p.price}€",
"callback_data": f"product_view_{p.id}",
},
"pagination": {
"callback": lambda p: f"products_page_{p}",
"hide_if_single_page": True,
},
})
.build()
)
await query.edit_message_reply_markup(
reply_markup=keyboard["reply_markup"]
)
# Register the handler
app.add_handler(CallbackQueryHandler(show_products, pattern=r"^products_page_\d+$"))
# Render — page 2/9
[ 🛍 Shoes Nike — 89€ ]
[ 🛍 Adidas Bag — 45€ ]
[ 🛍 Casio Watch — 120€ ]
[ 🛍 Ray-Ban — 99€ ]
[ 🛍 NY Cap — 25€ ]
[ ⬅️ ][ 2/9 ][ ➡️ ]
Paginated user list with edge buttons
For long lists (100+ items), show_edge_buttons lets users jump directly to the first or last page.
async def show_users(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
page = int(query.data.split("_")[-1])
users = await db.get_all_users()
keyboard = (
InlineKeyboardBuilder()
.paginated_list({
"items": users,
"page": page,
"per_page": 8,
"render": lambda u: {
"text": f"👤 {u.username} ({u.role})",
"callback_data": f"user_info_{u.id}",
},
"pagination": {
"callback": lambda p: f"users_page_{p}",
"show_edge_buttons": True,
"labels": {
"previous": "◀️",
"next": "▶️",
"first": "⏮",
"last": "⏭",
},
},
})
.build()
)
await query.edit_message_text(
"👥 Users",
reply_markup=keyboard["reply_markup"],
)
app.add_handler(CallbackQueryHandler(show_users, pattern=r"^users_page_\d+$"))
# Render — page 1/13, ⏮ and ◀️ are dimmed
[ 👤 alice (admin) ]
[ 👤 bob (user) ]
...
[ ·⏮· ][ ·◀️· ][ 1/13 ][ ▶️ ][ ⏭ ]
Dynamic search results
The search query is encoded directly in callback_data.
import re
async def show_search(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
match = re.match(r"^search_(.+)_page_(\d+)$", query.data)
term = match.group(1)
page = int(match.group(2))
results = await search(term)
if not results:
await query.answer("🚫 No results found")
return
keyboard = (
InlineKeyboardBuilder()
.paginated_list({
"items": results,
"page": page,
"per_page": 4,
"render": lambda r: {
"text": f"📄 {r.title}",
"callback_data": f"open_doc_{r.id}",
},
"pagination": {
"callback": lambda p: f"search_{term}_page_{p}",
"counter_callback": f"search_info_{term}",
"hide_if_single_page": True,
},
})
.build()
)
await query.edit_message_text(
f'🔍 Results for "{term}"',
reply_markup=keyboard["reply_markup"],
)
app.add_handler(CallbackQueryHandler(show_search, pattern=r"^search_.+_page_\d+$"))
⚠️ Telegram limit:
callback_datais capped at 64 bytes. If the search term can be long, store it in the conversation context (context.user_data["query"]) and encode only an ID incallback_data.
Premium button styles
Requires a Telegram Premium subscription for the bot owner.
keyboard = (
InlineKeyboardBuilder(buttons_per_row=1)
.add_callback_button("🔵 Primary", "action_1", style="primary")
.add_callback_button("🟢 Success", "action_2", style="success")
.add_callback_button("🔴 Danger", "action_3", style="danger")
.add_callback_button("Icon only", "action_4", icon_custom_emoji_id="4963511421280192936")
.add_callback_button("Icon + style", "action_5",
style="success",
icon_custom_emoji_id="4963511421280192936",
)
.build()
)
icon_custom_emoji_idonly works when the bot owner has an active Telegram Premium subscription.
Payment buttons
⚠️ Telegram limitation
[!WARNING] Payment buttons must only be used with
send_invoice. They must not appear in regular messages.
builder.add_pay_button("💳 Pay now")
Using a payment button outside an invoice causes a Telegram API error.
Common errors
Passing reply_markup incorrectly
keyboard = (
InlineKeyboardBuilder()
.add_callback_button("⚙️ Settings", "show_settings")
.build()
)
# ✅ Correct — pass the inner reply_markup value
await update.message.reply_text("Menu:", reply_markup=keyboard["reply_markup"])
# ✅ Also correct — spread into options dict
await update.message.reply_text("Menu:", **{
"reply_markup": keyboard["reply_markup"],
"parse_mode": "HTML",
})
# ❌ Wrong — passes the outer wrapper dict
await update.message.reply_text("Menu:", reply_markup=keyboard)
Migration to v3
v3 is fully backward compatible with v2. Existing code requires no changes.
All new features are opt-in. The constructor signature is unchanged.
| What you did in v2 | What you can use now (v3) |
|---|---|
Manual callback_data strings |
add_callback_button_from_parts(scope, action, id) |
print(builder.build()) to inspect |
builder.preview() — row-by-row readable output |
Parsing callback_data by hand |
callback_data_parse(data) → {scope, action, id} |
| Manual pagination + handler code | paginated_list({items, page, per_page, render, …}) |
v3 migration checklist
-
pip install telegram-inline-keyboard-builder --upgrade - Replace manual
callback_dataconcatenations withadd_callback_button_from_parts() - Replace hand-rolled pagination logic with
paginated_list() - Use
preview()during development to verify keyboard layout - Optionally add type hints using the exported types from
telegram_inline_keyboard_builder.types
Support this project
This project is maintained in my free time. If it helped you, consider supporting it with a crypto donation ❤️
| Crypto | Address |
|---|---|
| USDT (TRC20) | 0x607c1430601989d43c9CD2eeD9E516663e0BdD1F |
| USDC (Polygon/ETH) | 0x607c1430601989d43c9CD2eeD9E516663e0BdD1F |
| Ethereum (ETH) | 0x607c1430601989d43c9CD2eeD9E516663e0BdD1F |
| Bitcoin (BTC) | bc1qmysepz6eerz2mqyx5dd0yy87c3gk6hccwla5x2 |
| Tron (TRX) | TE9RiTaDpx7DGZzCMw7qds51nzszKiyeR8 |
| TON | UQA1NPW4GqgIVa9R6lebN_0v64Q-Sz_nHrmK9LCk-FfdjVOH |
🔹 Optional QR Codes for quick mobile donation
USDT (TRC20)
USDC
Ethereum (ETH)
Bitcoin (BTC)
Tron (TRX)
TON
Contribution
Contributions are welcome ❤️ Please open an issue before proposing major changes.
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
File details
Details for the file telegram_inline_keyboard_builder-3.0.0.tar.gz.
File metadata
- Download URL: telegram_inline_keyboard_builder-3.0.0.tar.gz
- Upload date:
- Size: 21.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c552dab093b507795a6e442b382a0efe1cd4ef594d003746ece89b400688069
|
|
| MD5 |
2345b400863261f8e54bb614fa33ec32
|
|
| BLAKE2b-256 |
5029ce1257347ed579cff18ca5706202f108e6538ba1759fc61eb819d5bb1791
|
File details
Details for the file telegram_inline_keyboard_builder-3.0.0-py3-none-any.whl.
File metadata
- Download URL: telegram_inline_keyboard_builder-3.0.0-py3-none-any.whl
- Upload date:
- Size: 18.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ce474365209c3f467b42eb44cc80111471997b6645d3fbf6266cb9e670ca8e50
|
|
| MD5 |
d63f89a8c9b54234bcd22a7e4525d930
|
|
| BLAKE2b-256 |
74f379a7f2a33c360ef1cf4f937937e1ce2235badd3d8b72be022056a0f3a9e7
|