mrkeyoor.com_
Thu 06 Aug 23:56 UTC
PyPIWeb Backendupdated 06 Aug 2026

python-telegram-bot

python-telegram-bot (PTB) is the standard Python wrapper for the Telegram Bot API. The telegram module mirrors every type and method of Bot API 10.0 one to one; the telegram.ext module adds the framework you actually build with: Application, handler classes for commands, messages, callback queries and multi-step conversations, a job queue, persistence, and rate limiting. Since v20 the whole library is asyncio-based: every handler is an async function and every bot method is awaited. The only required dependency is httpx; everything else (tornado for webhooks, APScheduler for the job queue, cachetools, aiolimiter) is an optional extra.

Verdict

The default choice for Telegram bots in Python and one of the best-run projects on PyPI. Commit to asyncio, install the extras you need, and ignore every tutorial written before 2023.

API stability3/5v20 (2023) was a full async rewrite that broke every existing bot, and since then versions move fast (22.4 in September 2025 to 22.8 in June 2026) with deprecations riding each Bot API release. The current API is coherent, but you pin versions and read the changelog on every bump.
Docs5/5Dedicated docs site with full API reference and changelog, a wiki with long-form guides (concurrency, webhooks, arbitrary callback_data), and an examples section released to the public domain; the README calls echobot.py the de facto base for most bots out there. The catch is discovery: search results still surface obsolete v13 material.
Maintenance5/5Pushed August 4, 2026; only 34 open issues counting PRs against roughly 29,400 stars; v22.8 shipped June 2026 with Bot API 10.0 support; releases are sigstore-signed since v21.4 and the test matrix already covers free-threaded Python 3.14.
Ecosystem4/5About 10M weekly downloads, a dedicated Stack Overflow tag, and an active Telegram support group make it the biggest Python Telegram library. Docked one point because aiogram is a genuine rival that keeps pulling new projects, and because telegram.ext is a closed framework with little third-party plugin culture around it.

Use it if

  • You are building a Telegram bot in Python that is more than a one-off script: the handler system, ConversationHandler, and job queue cover what you would otherwise hand-roll on top of raw HTTP calls
  • You want same-week Bot API coverage: the library tracks Telegram's API releases closely (Bot API 10.0 in v22.8) and documents a forward-compatibility path for anything not yet wrapped
  • You care about maintenance signals: pushed August 2026, roughly 29,400 stars, only 34 open issues and PRs combined, sigstore-signed releases since v21.4, and tested against Python 3.10 through 3.14 including free-threaded builds
  • Your bot is already async or lives next to other asyncio code (FastAPI, aiohttp), since PTB shares the event loop instead of spawning worker threads
Skip it if

Setup reality

pip install python-telegram-bot is pure Python and takes seconds on 3.10+. The traps come later: JobQueue silently does not exist (app.job_queue is None) until you install the job-queue extra for APScheduler, run_webhook raises until you install the webhooks extra for tornado, and arbitrary callback_data needs the callback-data extra. The one hard dependency, httpx, is pinned below 0.29 and can collide with whatever else in your project wants a newer httpx. The biggest cost is not install at all, it is that nearly every tutorial older than 2023 targets the removed Updater/dispatcher API, so you must work from the current docs and examples only.

Patterns

Smallest working botminimal-bot

from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    await update.message.reply_text("I am alive")

app = Application.builder().token("YOUR_BOT_TOKEN").build()
app.add_handler(CommandHandler("start", start))
app.run_polling()

run_polling() creates and manages the event loop itself, so there is no asyncio.run() wrapper and nothing after it executes until shutdown. The token comes from @BotFather. Every handler must be an async def and every bot call must be awaited.

Recognize and replace dead pre-v20 tutorial codeavoid-v13-updater

# Pre-2023 tutorials and most LLM output look like this. It does NOT run on v20+:
#   updater = Updater("TOKEN", use_context=True)
#   updater.dispatcher.add_handler(CommandHandler("start", start))
#   updater.start_polling(); updater.idle()

# Current API since v20:
app = Application.builder().token("TOKEN").build()
app.add_handler(CommandHandler("start", start))  # start is async def now
app.run_polling()

The library went fully async at v20 (January 2023). Updater still exists but only as the internal update fetcher; dispatcher, use_context, and sync handlers are gone. If a snippet has no async/await, it is for the old API and will fail with import or attribute errors.

Command handler that reads argumentscommand-arguments

async def caps(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    if not context.args:
        await update.message.reply_text("Usage: /caps some text")
        return
    await update.message.reply_text(" ".join(context.args).upper())

app.add_handler(CommandHandler("caps", caps))

context.args is the whitespace-split text after the command, already stripped of the command itself. In groups, /caps@YourBotName also matches, and Telegram only forwards group messages to the bot per its privacy mode unless that is disabled in @BotFather.

Message handlers with filtersmessage-filters

from telegram.ext import MessageHandler, filters

async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    await update.message.reply_text(update.message.text)

app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
app.add_handler(MessageHandler(filters.PHOTO, on_photo))
app.add_handler(MessageHandler(filters.Regex(r"(?i)refund"), on_refund))

The lowercase filters module replaced the v13 Filters class. Compose with &, |, ~. Keep ~filters.COMMAND on catch-all text handlers or they swallow commands too; within one handler group only the first match runs, so registration order matters.

Inline keyboard and callback query handlinginline-keyboard-callbacks

from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import CallbackQueryHandler

async def menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    kb = [[InlineKeyboardButton("Approve", callback_data="ok"),
           InlineKeyboardButton("Reject", callback_data="no")]]
    await update.message.reply_text("Decide:", reply_markup=InlineKeyboardMarkup(kb))

async def on_button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    query = update.callback_query
    await query.answer()
    await query.edit_message_text(f"You chose: {query.data}")

app.add_handler(CommandHandler("menu", menu))
app.add_handler(CallbackQueryHandler(on_button))

Always await query.answer(), even with no text, or the user's client shows a loading spinner for up to a minute. callback_data is capped at 64 bytes by Telegram; for real objects install the callback-data extra and enable arbitrary_callback_data in the builder.

Multi-step conversationconversation-handler

from telegram.ext import ConversationHandler

NAME, AGE = range(2)

async def signup(update, context):
    await update.message.reply_text("Your name?")
    return NAME

async def got_name(update, context):
    context.user_data["name"] = update.message.text
    await update.message.reply_text("Your age?")
    return AGE

async def got_age(update, context):
    await update.message.reply_text(f"Done, {context.user_data['name']}.")
    return ConversationHandler.END

async def cancel(update, context):
    return ConversationHandler.END

app.add_handler(ConversationHandler(
    entry_points=[CommandHandler("signup", signup)],
    states={NAME: [MessageHandler(filters.TEXT & ~filters.COMMAND, got_name)],
            AGE: [MessageHandler(filters.Regex(r"^\d+$"), got_age)]},
    fallbacks=[CommandHandler("cancel", cancel)],
))

Each handler returns the next state, or ConversationHandler.END. State lives in memory per chat, so a restart drops every conversation mid-flow unless you configure a persistence class. Always ship a fallback; users who get stuck in a state otherwise have no exit.

Send photos and documentssend-files-photos

from pathlib import Path

async def report(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    msg = await update.message.reply_photo(
        photo=Path("chart.png"), caption="Daily chart")
    context.bot_data["chart_file_id"] = msg.photo[-1].file_id

    await context.bot.send_document(
        chat_id=update.effective_chat.id,
        document=open("report.pdf", "rb"),
        filename="report.pdf")

photo/document accept a Path, a file object, raw bytes, an HTTP URL, or a Telegram file_id. Reuse the file_id from the returned Message to resend without re-uploading. Bot API limits: 50 MB uploads, 20 MB downloads; photos get recompressed, so send originals as documents.

Run on a webhook instead of pollingwebhook-vs-polling

# pip install "python-telegram-bot[webhooks]"   (pulls tornado)
app.run_webhook(
    listen="127.0.0.1",
    port=8443,
    url_path="telegram",
    secret_token="random-string-you-generate",
    webhook_url="https://example.com/telegram",
)

run_polling and run_webhook are interchangeable at the end of the same setup, so develop on polling and deploy on webhooks. Telegram only delivers to HTTPS on ports 443, 80, 88, or 8443, so in practice you sit behind nginx for TLS. The secret_token check is how you reject forged POSTs.

Scheduled and repeating jobsjob-queue

# pip install "python-telegram-bot[job-queue]"   (pulls APScheduler)

async def remind(context: ContextTypes.DEFAULT_TYPE) -> None:
    await context.bot.send_message(chat_id=context.job.chat_id, text="Reminder!")

async def set_timer(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    context.job_queue.run_once(remind, when=300, chat_id=update.effective_chat.id)

app.job_queue.run_repeating(nightly_sync, interval=86400, first=10)

Without the job-queue extra, app.job_queue is simply None and the first attribute access dies with a confusing AttributeError. Job callbacks receive only context, no update; pass chat_id or data into run_once/run_repeating and read them back off context.job. Jobs are in-memory and do not survive restarts.

Register an error handlererror-handler

import logging

logger = logging.getLogger(__name__)

async def on_error(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
    logger.error("Update %s caused error", update, exc_info=context.error)
    # optionally: await context.bot.send_message(chat_id=DEV_CHAT_ID, text=str(context.error))

app.add_error_handler(on_error)

Without this, handler exceptions are only logged and the bot keeps running as if nothing happened, so bugs go unnoticed for days. Transient network errors (telegram.error.TimedOut, NetworkError) land here too; do not treat every invocation as a crash.

Send a message from synchronous codesync-code-bridge

import asyncio
from telegram import Bot

def notify(text: str) -> None:
    async def _send():
        async with Bot("YOUR_BOT_TOKEN") as bot:
            await bot.send_message(chat_id=CHAT_ID, text=text)
    asyncio.run(_send())

There is no sync API since v20, so scripts and cron jobs need this bridge. asyncio.run() per call is fine for one-shot notifications but do not call it from inside a running event loop; in async code just use the Bot instance directly.

Process updates concurrentlyconcurrent-updates

app = (
    Application.builder()
    .token("YOUR_BOT_TOKEN")
    .concurrent_updates(True)
    .build()
)

By default updates are handled strictly one at a time, so one slow handler (an API call, a download) stalls every other chat. concurrent_updates(True) processes up to 256 updates at once, but per-update ordering guarantees are gone and shared state in user_data/bot_data now needs care.

Alternatives

PackageRegistryPick it when
aiogramPyPIYou want the other modern async framework: router and middleware architecture with a built-in FSM instead of PTB's handler groups
pyTelegramBotAPIPyPIYou want a synchronous API for a small script and refuse to touch asyncio (it also ships an async variant)
TelethonPyPIYou need MTProto: user accounts, channels history, or files past the Bot API's 20 MB download and 50 MB upload limits