puragram
Fast, dependency-light Telegram Bot API framework built on urllib3. No aiohttp. No requests. No httpx.
Why puragram?
Most Telegram Python libraries pull in aiohttp or requests. puragram
talks directly to the Telegram Bot API through urllib3 with a keep-alive
connection pool. That means:
- Small — no heavy dependencies, just
urllib3 - Fast — direct JSON requests, no middleware layers
- Sync — simple, predictable, easy to debug
Install
pip install puragram
Requires Python 3.9+.
Quick start
from puragram import Bot
bot = Bot("YOUR_TOKEN", parse_mode="HTML")
@bot.message_handler(commands=["start"])
def start(msg):
bot.send_message(msg.chat.id, f"Hi, <b>{msg.from_user.first_name}</b>!")
@bot.message_handler(content_types=["text"])
def echo(msg):
bot.send_message(msg.chat.id, f"You said: <b>{msg.text}</b>")
if __name__ == "__main__":
bot.run_polling()
FSM example
from puragram import Bot, RemoveKeyboard, State, StatesGroup
bot = Bot("YOUR_TOKEN", parse_mode="HTML")
class Form(StatesGroup):
name = State()
age = State()
@bot.message_handler(commands=["start"])
def start(msg, data):
data["state"].set_state(Form.name)
bot.send_message(msg.chat.id, "What's your name?")
@bot.message_handler(state=Form.name)
def on_name(msg, data):
data["state"].update_data(name=msg.text)
data["state"].set_state(Form.age)
bot.send_message(msg.chat.id, f"Nice to meet you, {msg.text}. How old are you?")
@bot.message_handler(state=Form.age)
def on_age(msg, data):
ctx = data["state"]
if not msg.text.isdigit():
bot.send_message(msg.chat.id, "Please enter a number.")
return
ctx.update_data(age=int(msg.text))
info = ctx.get_data()
ctx.clear()
bot.send_message(
msg.chat.id,
f"Done!\nName: {info['name']}\nAge: {info['age']}",
reply_markup=RemoveKeyboard(),
)
bot.run_polling()
Inline mode
from puragram import Bot, InlineQueryResultArticle
bot = Bot("YOUR_TOKEN")
@bot.inline_query_handler()
def on_inline(q):
results = [
InlineQueryResultArticle(
id="hello",
title="Send hello",
input_message_content={"message_text": "Hello!"},
),
]
bot.answer_inline_query(q.id, results)
bot.run_polling()
Middleware
from puragram import Bot, LoggingMiddleware, ThrottlingMiddleware
bot = Bot("YOUR_TOKEN")
bot.middleware(LoggingMiddleware())
bot.middleware(ThrottlingMiddleware(rate=0.5))
@bot.message_handler(content_types=["text"])
def echo(msg):
bot.send_message(msg.chat.id, msg.text)
bot.run_polling(workers=4)
Webhook
from puragram import Bot, WebhookServer
bot = Bot("YOUR_TOKEN")
@bot.message_handler(commands=["start"])
def start(msg):
bot.send_message(msg.chat.id, "Hello via webhook!")
if __name__ == "__main__":
server = WebhookServer(
bot,
host="0.0.0.0",
port=8080,
path="/webhook",
secret_token="change-me-to-random-32-chars",
)
server.install("https://your-domain.com/webhook")
server.start(blocking=True)
Features
- Long-polling and webhook (pure stdlib
http.server) - Inline mode:
@yourbot querywith results - FSM:
State,StatesGroup,MemoryStorage,FileStorage,SQLiteStorage - Middleware: logging, throttling, timing
- Filters:
Command,Text,Regexp,ContentTypes,ChatType,ChatId,UserId,CallbackData,CallbackDataPrefix,Func - Filter operators:
&(and),|(or),~(not) - File sending: photo, document, video, audio, voice, sticker
- Extras: poll, location, contact, dice
- Auto-split for long messages:
send_long_message - Security: path traversal guard, size limits, ReDoS protection, token redaction, dedup, safe SQLite
- Zero dependencies except
urllib3
Performance
Benchmarked on Termux (Android, same Wi-Fi, ~30 ms to Telegram, median of 3 trials):
| Workload | puragram | pyTelegramBotAPI |
|---|---|---|
| 5 × sendMessage | 85 ms/msg | 89 ms/msg |
| 3 × sendPhoto | 99 ms/msg | 116 ms/msg |
Your results will vary with network latency.
Security
- Path traversal —
safe_path()rejects/etc,/proc,/sys,/root,/dev - Upload size — 50 MB limit enforced
- Callback data — validated to ≤64 UTF-8 bytes
- ReDoS —
compile_safe_regex()rejects nested quantifiers - Webhook forgery —
hmac.compare_digeston secret token - Token leakage — bot tokens redacted from logs
- Idempotency — duplicate
update_iddropped via LRU - SQL injection — parameterized queries only
Documentation
Full beginner's guide: ABOUT.md.
Changelog: CHANGELOG.md.
License
MIT — see LICENSE.
Release files for puragram 1.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| puragram-1.2.0.tar.gz | 29.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| puragram-1.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 56.7 kB
Release files / puragram-1.2.0.tar.gz
| Download URL | puragram-1.2.0.tar.gz |
|---|---|
| Size | 29.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b4b69d56b19129fcb7374451d070f51e1855459e2882a13bc232361903f35925
|
|
BLAKE2b-256 checksum How to use checksums |
efccf6458607dbf957b78cf9359b088ca731c8ca927294d636a168e79a1a8581
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|
Release files / puragram-1.2.0-py3-none-any.whl
| Download URL | puragram-1.2.0-py3-none-any.whl |
|---|---|
| Size | 26.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1527a964f21e9f5c096f0f5de721203e00d7d438d66975715427de63ccefa4fa
|
|
BLAKE2b-256 checksum How to use checksums |
34097af0bdb9a4edf8fd9d68c98c257b2526f74f3fab63c9029fb2a46e69fed5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|