A simple and convenient library for creating Telegram bots.
Project description
Extergram — Asynchronous Telegram Bot Framework
Disclaimer: This project is an independent open-source library and is not affiliated with, associated with, authorized by, endorsed by, or in any way officially connected with Telegram FZ-LLC or any of its subsidiaries or its affiliates.
Extergram is a modern, simple, and fully asynchronous library for creating Telegram bots in Python using httpx. It provides a clean API, built-in FSM (Finite State Machine) with multiple storage backends, anti-flood protection, support for both inline and reply keyboards, and a wide range of Telegram Bot API methods.
Current Version: 0.9.0
Installation
Extergram requires Python 3.8+.
pip install extergram
Optional dependencies for persistent FSM storage:
- Redis:
pip install redis - JSON and SQLite storages work out of the box (no extra packages required).
Quick Start
A minimal echo bot:
import asyncio
from extergram import Bot, ContextTypes
from extergram.ext import CommandHandler, MessageHandler
async def start(context: ContextTypes):
await context.bot.send_message(
chat_id=context.message.chat.id,
text="Hello! I am an Extergram bot."
)
async def echo(context: ContextTypes):
await context.bot.send_message(
chat_id=context.message.chat.id,
text=f"You said: {context.message.text}"
)
async def main():
bot = Bot(token="YOUR_BOT_TOKEN")
bot.add_handler(CommandHandler("start", start))
bot.add_handler(MessageHandler(echo))
await bot.polling()
if __name__ == "__main__":
asyncio.run(main())
Note: Starting from version 0.9.0, the default parse_mode is "MarkdownV2". To send messages without formatting, pass parse_mode="" explicitly.
API Reference
Core Concepts
- Bot – the main class that interacts with Telegram API.
- ContextTypes – passed to handlers, provides access to bot, update, and FSM.
- Handlers – define how to process updates (messages, commands, callbacks). Handlers are checked sequentially; the first matching handler is executed.
- FSM – built-in finite state machine for multi-step dialogs, with optional persistent storage (Memory, JSON, SQLite, Redis).
1. Bot Class
Constructor
Bot(token: str, default_parse_mode: str = None, fsm_storage: FSMStorage = None)
token– your bot token from BotFather.default_parse_mode– default parse mode for messages ("HTML","MarkdownV2", or"Markdown"). If not set,"MarkdownV2"is used. Pass""to disable formatting.fsm_storage– an instance ofFSMStorage(e.g.,MemoryFSMStorage,RedisFSMStorage). Defaults toMemoryFSMStorageif not provided.
Methods
Polling
async polling(timeout: int = 30)
Starts long polling. This coroutine runs forever until interrupted.
Handler Registration
add_handler(handler: BaseHandler)
Registers a handler. Must be an instance of BaseHandler.
Getting Updates
async get_updates(offset: int = None, timeout: int = 30) -> List[dict]
Fetches raw updates from Telegram. Normally you don't need to call this directly.
Sending Messages
async send_message(
chat_id: int,
text: str,
parse_mode: str = None,
disable_web_page_preview: bool = None,
disable_notification: bool = None,
reply_to_message_id: int = None,
reply_markup = None,
message_thread_id: int = None,
business_connection_id: str = None
) -> dict
parse_mode: IfNone, the bot'sdefault_parse_modeis used. Pass""to send plain text.reply_markup: Can be aButtonsDesigninstance (inline keyboard), aReplyKeyboardinstance (reply keyboard), or a raw dict.
Sending Media
async send_photo(chat_id: int, photo: str, caption: str = None, parse_mode: str = None, reply_markup=None) -> dict
async send_document(chat_id: int, document: str, caption: str = None, parse_mode: str = None, reply_markup=None) -> dict
async send_video(chat_id: int, video: str, caption: str = None, parse_mode: str = None, reply_markup=None) -> dict
async send_animation(chat_id: int, animation: str, caption: str = None, parse_mode: str = None, reply_markup=None) -> dict
async send_voice(chat_id: int, voice: str, caption: str = None, parse_mode: str = None, reply_markup=None) -> dict
async send_video_note(chat_id: int, video_note: str, reply_markup=None) -> dict
async send_sticker(chat_id: int, sticker: str, reply_markup=None, disable_notification: bool = None, message_thread_id: int = None) -> dict
All media methods accept a local file path or an HTTP URL. Local files are uploaded as multipart/form-data.
Sending Other Content
async send_location(
chat_id: int,
latitude: float,
longitude: float,
live_period: int = None,
disable_notification: bool = None,
message_thread_id: int = None,
reply_markup = None
) -> dict
async send_venue(
chat_id: int,
latitude: float,
longitude: float,
title: str,
address: str,
foursquare_id: str = None,
disable_notification: bool = None,
message_thread_id: int = None,
reply_markup = None
) -> dict
async send_contact(
chat_id: int,
phone_number: str,
first_name: str,
last_name: str = None,
disable_notification: bool = None,
message_thread_id: int = None,
reply_markup = None
) -> dict
async send_poll(
chat_id: int,
question: str,
options: list,
is_anonymous: bool = True,
type: str = 'regular',
allows_multiple_answers: bool = False,
correct_option_id: int = None,
explanation: str = None,
open_period: int = None,
close_date: int = None,
is_closed: bool = False,
disable_notification: bool = None,
message_thread_id: int = None,
reply_markup = None
) -> dict
async send_dice(
chat_id: int,
emoji: str = '🎲',
disable_notification: bool = None,
message_thread_id: int = None,
reply_markup = None
) -> dict
Message Draft (Forum Topics)
async send_message_draft(
chat_id: int,
draft_id: int,
text: str,
parse_mode: str = None,
entities: list = None,
message_thread_id: int = None
) -> dict
Streams a partial message to a chat (for bots with forum topic mode enabled). The draft_id must be a non-zero unique identifier.
Media Groups
async send_media_group(
chat_id: int,
media: list,
disable_notification: bool = None,
message_thread_id: int = None
) -> dict
mediais a list of dictionaries representing InputMedia objects (e.g.,{"type": "photo", "media": "file_id_or_url"}).
Forwarding and Copying Messages
async forward_message(
chat_id: int,
from_chat_id: int,
message_id: int,
disable_notification: bool = None,
message_thread_id: int = None
) -> dict
async copy_message(
chat_id: int,
from_chat_id: int,
message_id: int,
caption: str = None,
parse_mode: str = None,
disable_notification: bool = None,
message_thread_id: int = None,
reply_markup = None
) -> dict
Editing Messages
async edit_message_text(
chat_id: int,
message_id: int,
text: str,
parse_mode: str = None,
reply_markup = None
) -> dict
async edit_message_caption(
chat_id: int,
message_id: int,
caption: str,
parse_mode: str = None,
reply_markup = None,
show_caption_above_media: bool = None
) -> dict
async edit_message_media(
chat_id: int,
message_id: int,
media: dict,
reply_markup = None,
business_connection_id: str = None
) -> dict
async edit_message_reply_markup(
chat_id: int,
message_id: int,
reply_markup = None
) -> dict
Deleting Messages
async delete_message(chat_id: int, message_id: int) -> dict
async delete_messages(chat_id: int, message_ids: list) -> dict
Callback Queries
async answer_callback_query(
callback_query_id: str,
text: str = None,
show_alert: bool = False,
url: str = None,
cache_time: int = None
) -> dict
Polls
async stop_poll(chat_id: int, message_id: int, reply_markup=None) -> dict
Bot Commands
async set_my_commands(commands: List[BotCommand]) -> dict
Administration
async ban_chat_member(
chat_id: int,
user_id: int,
until_date: int = None,
revoke_messages: bool = None
) -> dict
async unban_chat_member(
chat_id: int,
user_id: int,
only_if_banned: bool = None
) -> dict
async restrict_chat_member(
chat_id: int,
user_id: int,
permissions: ChatPermissions,
until_date: int = None
) -> dict
async promote_chat_member(
chat_id: int,
user_id: int,
**permissions
) -> dict
async approve_chat_join_request(chat_id: int, user_id: int) -> dict
async decline_chat_join_request(chat_id: int, user_id: int) -> dict
2. ContextTypes
Passed to handlers (callback receives either (bot, event) or (context,)). Provides:
bot– the Bot instance.update– the raw Update object.message– the Message object (if present).callback_query– the CallbackQuery object (if present).effective_user– the user who triggered the update.effective_chat– the chat where the update occurred.state– anFSMContextinstance for the current user/chat.
3. Data Classes
These classes mirror Telegram API objects. They are created automatically from incoming updates.
- User:
id,is_bot,first_name,last_name,username - Chat:
id,type,title,username - Message:
message_id,from_user,chat,date,text,caption - CallbackQuery:
id,from_user,message,data - Update:
update_id,message,edited_message,callback_query - BotCommand: simple container for commands.
- ChatPermissions: used with
restrict_chat_member. All permissions default toNone(meaning "don't change"). UseTrueto allow,Falseto disallow.
Example:
perms = ChatPermissions(can_send_messages=False, can_send_photos=False)
4. UI: Keyboards
Extergram supports both inline and reply keyboards.
Inline Keyboard (ButtonsDesign)
from extergram import ButtonsDesign
kb = ButtonsDesign()
kb.add_row(
ButtonsDesign.create_button("Click me", "callback_data"),
ButtonsDesign.create_url_button("GitHub", "https://github.com")
)
create_button(text, callback_data)create_url_button(text, url)add_row(*buttons)– adds a row of buttons.to_dict()– returns the keyboard as a dict for the API.
Reply Keyboard (ReplyKeyboard & KeyboardButton)
from extergram import ReplyKeyboard, KeyboardButton
kb = ReplyKeyboard(resize_keyboard=True, input_field_placeholder="Choose an option")
kb.add_row(
KeyboardButton("Send my phone", request_contact=True),
KeyboardButton("Share location", request_location=True)
)
ReplyKeyboard(resize_keyboard=False, one_time_keyboard=False, selective=False, input_field_placeholder=None)add_row(*buttons)– adds a row. Buttons can be plain strings,KeyboardButtonobjects, or dicts.KeyboardButton(text, request_contact=False, request_location=False, web_app=None, request_users=None, request_chat=None, request_poll=None)– only one optional field can be active at a time.
5. Utilities
Markdown Helper
Safely builds MarkdownV2 strings with automatic escaping.
from extergram import Markdown
text = str(
Markdown("Hello ")
.bold("World")
.text(" this is ")
.italic("escaped")
)
await bot.send_message(chat_id, text) # parse_mode is already "MarkdownV2" by default
Methods:
text(text)– appends plain text (escaped).bold(text)– appends bold text.italic(text)– appends italic text.__str__()– returns the final string.
escape_markdown_v2(text)
Low-level function to escape MarkdownV2 special characters.
6. Documentation Access
from extergram import Docs
print(Docs.get_docs()) # returns a link to GitHub README
Docs.print_docs() # prints the link
7. Handlers (extergram.ext)
All handlers inherit from BaseHandler and must implement check_update. They are registered via bot.add_handler().
- BaseHandler – abstract base class.
- MessageHandler – triggers on any text message.
- CommandHandler(command, callback) – triggers on a command (e.g.,
/start). Accepts a single command string or a list of strings. - CallbackQueryHandler – triggers on any callback query.
- StateHandler(state, handler) – wraps another handler and only triggers if the user is in a specific FSM state. Must be used with FSM storage.
Example:
from extergram.ext import StateHandler, MessageHandler
bot.add_handler(StateHandler("waiting_for_name", MessageHandler(process_name)))
8. FSM (Finite State Machine)
Extergram provides a flexible FSM with swappable storage backends.
Storage Backends
- MemoryFSMStorage – in-memory, volatile. State is lost on restart.
- JSONFSMStorage – persists state and data to a JSON file. Constructor:
JSONFSMStorage(file_path="fsm_data.json"). - SQLiteFSMStorage – persists state to a SQLite database. Constructor:
SQLiteFSMStorage(db_path="fsm_storage.db"). - RedisFSMStorage – persists state to a Redis server (requires
pip install redis). Constructor:RedisFSMStorage(redis_url="redis://localhost:6379/0", **kwargs).
You can pass your chosen storage to the bot:
from extergram import Bot, RedisFSMStorage
bot = Bot(token="TOKEN", fsm_storage=RedisFSMStorage("redis://localhost:6379"))
If no storage is specified, MemoryFSMStorage is used by default.
FSMContext
Obtained via context.state. Methods:
async get_state() -> Optional[str]async set_state(state: Optional[str])– passNoneto reset state.async get_data() -> dictasync set_data(data: dict)async update_data(**kwargs)async clear()– clears both state and data.
9. Exceptions
All exceptions inherit from extergram.errors.ExtergramError.
APIError– base for API errors. Containsdescription,error_code, and optionalparameters(e.g.,retry_after).NetworkError– network-related issues.BadRequestError(400)UnauthorizedError(401)ForbiddenError(403)NotFoundError(404)ConflictError(409) – another bot instance is running.EntityTooLargeError(413)FloodControlError(429) – too many requests. The library automatically retries afterretry_afterseconds (up to 5 times). If all retries fail, this exception is raised.InternalServerError(500)BadGatewayError(502)TelegramAdminError– insufficient rights for admin actions (subclass ofForbiddenError).
Anti-Flood System
Extergram automatically tracks request frequency and introduces dynamic delays when approaching Telegram's rate limits. When a 429 response is received, the library waits for the duration specified in retry_after and retries the request up to 5 times. This ensures smooth operation under high load.
Examples
Example 1: Echo Bot with Commands
import asyncio
from extergram import Bot, ContextTypes
from extergram.ext import CommandHandler, MessageHandler
async def start(context: ContextTypes):
await context.bot.send_message(
context.message.chat.id,
"Welcome! Send me any text and I'll echo it."
)
async def echo(context: ContextTypes):
await context.bot.send_message(
context.message.chat.id,
f"Echo: {context.message.text}"
)
async def main():
bot = Bot(token="YOUR_TOKEN")
bot.add_handler(CommandHandler("start", start))
bot.add_handler(MessageHandler(echo))
await bot.polling()
asyncio.run(main())
Example 2: Inline Keyboard with Callback
import asyncio
from extergram import Bot, ContextTypes, ButtonsDesign
from extergram.ext import CommandHandler, CallbackQueryHandler
async def start(context: ContextTypes):
kb = ButtonsDesign().add_row(
ButtonsDesign.create_button("Green", "color_green"),
ButtonsDesign.create_button("Red", "color_red")
)
await context.bot.send_message(
context.message.chat.id,
"Choose a color:",
reply_markup=kb
)
async def button_handler(context: ContextTypes):
query = context.callback_query
color = query.data.split('_')[1]
await context.bot.answer_callback_query(query.id, text=f"You chose {color}")
await context.bot.send_message(
query.message.chat.id,
f"Your favorite color is {color}!"
)
async def main():
bot = Bot(token="YOUR_TOKEN")
bot.add_handler(CommandHandler("start", start))
bot.add_handler(CallbackQueryHandler(button_handler))
await bot.polling()
asyncio.run(main())
Example 3: FSM – Collecting User Info (with JSON Storage)
import asyncio
from extergram import Bot, ContextTypes, JSONFSMStorage
from extergram.ext import CommandHandler, MessageHandler, StateHandler
async def start(context: ContextTypes):
await context.state.set_state("waiting_for_name")
await context.bot.send_message(context.message.chat.id, "What's your name?")
async def process_name(context: ContextTypes):
name = context.message.text
await context.state.update_data(name=name)
await context.state.set_state("waiting_for_age")
await context.bot.send_message(context.message.chat.id, f"Nice to meet you, {name}! How old are you?")
async def process_age(context: ContextTypes):
try:
age = int(context.message.text)
except ValueError:
await context.bot.send_message(context.message.chat.id, "Please enter a number.")
return
data = await context.state.get_data()
name = data.get("name")
await context.state.clear()
await context.bot.send_message(context.message.chat.id, f"Thank you! You are {name}, {age} years old.")
async def main():
# State will be saved to fsm_data.json
bot = Bot(token="YOUR_TOKEN", fsm_storage=JSONFSMStorage("fsm_data.json"))
bot.add_handler(CommandHandler("start", start))
bot.add_handler(StateHandler("waiting_for_name", MessageHandler(process_name)))
bot.add_handler(StateHandler("waiting_for_age", MessageHandler(process_age)))
await bot.polling()
asyncio.run(main())
Example 4: Sending Photo and Document
import asyncio
from extergram import Bot, ContextTypes
from extergram.ext import CommandHandler
async def photo(context: ContextTypes):
await context.bot.send_photo(
context.message.chat.id,
photo="https://picsum.photos/400/300",
caption="Random photo"
)
async def doc(context: ContextTypes):
await context.bot.send_document(
context.message.chat.id,
document="/path/to/file.pdf",
caption="My document"
)
async def main():
bot = Bot(token="YOUR_TOKEN")
bot.add_handler(CommandHandler("photo", photo))
bot.add_handler(CommandHandler("doc", doc))
await bot.polling()
asyncio.run(main())
Example 5: Admin Command with Error Handling
import asyncio
from extergram import Bot, ContextTypes, errors
from extergram.ext import CommandHandler
async def kick(context: ContextTypes):
if not context.message.reply_to_message:
await context.bot.send_message(context.message.chat.id, "Reply to a user's message to kick them.")
return
user_id = context.message.reply_to_message.from_user.id
try:
await context.bot.ban_chat_member(context.message.chat.id, user_id)
await context.bot.send_message(context.message.chat.id, f"User {user_id} has been kicked.")
except errors.TelegramAdminError as e:
await context.bot.send_message(context.message.chat.id, f"Failed: {e}")
async def main():
bot = Bot(token="YOUR_TOKEN")
bot.add_handler(CommandHandler("kick", kick))
await bot.polling()
asyncio.run(main())
Example 6: Reply Keyboard with Location and Contact
import asyncio
from extergram import Bot, ContextTypes, ReplyKeyboard, KeyboardButton
from extergram.ext import CommandHandler, MessageHandler
async def start(context: ContextTypes):
kb = ReplyKeyboard(resize_keyboard=True, input_field_placeholder="Select an option")
kb.add_row(
KeyboardButton("Send my phone", request_contact=True),
KeyboardButton("Share location", request_location=True)
)
await context.bot.send_message(context.message.chat.id, "Use the buttons below:", reply_markup=kb)
async def handle_message(context: ContextTypes):
if context.message.contact:
await context.bot.send_message(context.message.chat.id, f"Got your phone: {context.message.contact.phone_number}")
elif context.message.location:
await context.bot.send_message(context.message.chat.id, "Location received!")
else:
await context.bot.send_message(context.message.chat.id, "Please use the keyboard buttons.")
async def main():
bot = Bot(token="YOUR_TOKEN")
bot.add_handler(CommandHandler("start", start))
bot.add_handler(MessageHandler(handle_message))
await bot.polling()
asyncio.run(main())
Example 7: Dice and Poll
import asyncio
from extergram import Bot, ContextTypes
from extergram.ext import CommandHandler
async def dice(context: ContextTypes):
await context.bot.send_dice(context.message.chat.id, emoji="🎲")
async def poll(context: ContextTypes):
await context.bot.send_poll(
context.message.chat.id,
question="What's your favorite language?",
options=["Python", "JavaScript", "Go"],
is_anonymous=False
)
async def main():
bot = Bot(token="YOUR_TOKEN")
bot.add_handler(CommandHandler("dice", dice))
bot.add_handler(CommandHandler("poll", poll))
await bot.polling()
asyncio.run(main())
Example 8: Media Group
import asyncio
from extergram import Bot, ContextTypes
from extergram.ext import CommandHandler
async def album(context: ContextTypes):
media = [
{"type": "photo", "media": "https://picsum.photos/id/1/400/300"},
{"type": "photo", "media": "https://picsum.photos/id/2/400/300"}
]
await context.bot.send_media_group(context.message.chat.id, media)
async def main():
bot = Bot(token="YOUR_TOKEN")
bot.add_handler(CommandHandler("album", album))
await bot.polling()
asyncio.run(main())
License
This project is licensed under the MIT License. See the LICENSE file for details.
Links
- GitHub: https://github.com/TIBI624/extergram
- Issue Tracker: https://github.com/TIBI624/extergram/issues
- PyPI: https://pypi.org/project/extergram/
Project details
Release history Release notifications | RSS feed
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 extergram-0.9.0.tar.gz.
File metadata
- Download URL: extergram-0.9.0.tar.gz
- Upload date:
- Size: 30.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8c6118eeac36bdef3618cdc878053e36e1d015ea18a7469f27edfcf5db02eb45
|
|
| MD5 |
a1e8cbe43fada250b2b500d82ae3632b
|
|
| BLAKE2b-256 |
ecd349408ee9a2a47aad651e2571988701a4e6feb1b91cbecb1ba6c37e910792
|
File details
Details for the file extergram-0.9.0-py3-none-any.whl.
File metadata
- Download URL: extergram-0.9.0-py3-none-any.whl
- Upload date:
- Size: 27.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c74f27f8ae0daa41d9ebcab72e5073c6c0c21c743426ecd96923ea45b64ecb2
|
|
| MD5 |
c7c0251f220256642406abd09e656dde
|
|
| BLAKE2b-256 |
51c80b8ccbd17f013e5c4d94889d5f96399d93e63265b8d7cbe0fa30b6aa6733
|