Introduction to Telegram Bots and python-telegram-bot
Telegram bots are automated programs that run inside the Telegram messaging platform, capable of responding to messages, executing commands, sending notifications, and even processing payments. They act as virtual assistants that can serve millions of users simultaneously without human intervention. The python-telegram-bot library — often abbreviated as PTB — is the most popular, actively maintained Python wrapper for the Telegram Bot API. It provides a clean, asynchronous-first architecture that abstracts away the complexities of HTTP requests, JSON parsing, and webhook management, letting you focus entirely on your bot's logic.
Why python-telegram-bot Matters
Among the available Python libraries for Telegram bots, PTB stands out for several reasons. It offers a pure Python interface to every Telegram Bot API method, supports both polling and webhook modes out of the box, ships with a powerful handler system for dispatching updates, and fully embraces Python's asyncio ecosystem for high-performance concurrent operations. The library is backed by an active community, receives frequent updates to match Telegram's evolving API, and includes built-in utilities like conversation handlers, job queues for scheduled tasks, and persistence layers for storing user data. Whether you're building a simple echo bot or a complex multi-step onboarding flow with inline keyboards and payment processing, PTB gives you the tools to do it efficiently.
Setting Up Your Development Environment
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Prerequisites
Before writing any code, ensure you have Python 3.8 or newer installed on your system. You'll also need a Telegram account to register your bot and obtain an API token. The entire setup process takes about ten minutes from start to finish.
Installing the Library
Create a new project directory and set up a virtual environment to keep dependencies isolated. Then install python-telegram-bot along with its optional webhook dependency if you plan to deploy with a web server:
# Create project directory
mkdir my-telegram-bot
cd my-telegram-bot
# Create and activate virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install python-telegram-bot
pip install python-telegram-bot
# Optional: install webhook support with aiohttp
pip install python-telegram-bot[webhooks]
Registering Your Bot with BotFather
Open Telegram and search for @BotFather — the official bot creation service. Start a conversation and follow these steps to create a new bot:
- Send the command
/newbotand follow the prompts to name your bot - Choose a username ending with
bot, such asMyAwesomeHelperBot - Upon success, BotFather will return an API token that looks like
1234567890:ABCdefGHIjklMNOpqrsTUVwxyz - Save this token securely — it is the unique key that grants full control over your bot
Never commit your bot token to version control. Store it in environment variables or a .env file that is excluded from your repository. For local development, you can export it directly in your shell:
export BOT_TOKEN="1234567890:ABCdefGHIjklMNOpqrsTUVwxyz"
Creating Your First Telegram Bot
A Minimal Echo Bot
Let's start with the simplest possible bot: one that echoes back any text message the user sends. This example demonstrates the core pattern of creating an Application, registering a handler, and running the polling loop. Create a file named bot.py and add the following code:
import os
from telegram import Update
from telegram.ext import Application, MessageHandler, filters, ContextTypes
# Your bot token from BotFather, stored as an environment variable
BOT_TOKEN = os.environ.get("BOT_TOKEN")
async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Echo back whatever text the user sends."""
user_message = update.message.text
await update.message.reply_text(f"You said: {user_message}")
def main():
# Create the Application instance
app = Application.builder().token(BOT_TOKEN).build()
# Register a handler for text messages (excluding commands)
echo_handler = MessageHandler(filters.TEXT & ~filters.COMMAND, echo)
app.add_handler(echo_handler)
print("Bot is running. Press Ctrl+C to stop.")
# Start polling for updates from Telegram
app.run_polling()
if __name__ == "__main__":
main()
Run the bot with python bot.py. Open your bot's chat on Telegram, send a message like "Hello, world!" and you'll receive "You said: Hello, world!" in response. This simple foundation already handles network errors, retries, and graceful shutdown — all provided by the library's polling infrastructure.
Understanding the Building Blocks
The code above introduces four essential components of python-telegram-bot:
- Application: The central orchestrator that manages handlers, dispatches incoming updates, and runs the polling or webhook server
- Update: An object representing a single incoming event — a message, a callback query from an inline button, a new chat member, etc.
- ContextTypes.DEFAULT_TYPE: A context object that carries bot data, user data, and chat data across handler calls, and provides access to the bot instance for sending messages
- Handler: A rule-based dispatcher that matches specific update types and triggers the corresponding callback function
Building a Feature-Rich Bot
Adding Command Handlers
Commands are messages that start with a forward slash, like /start or /help. PTB provides the CommandHandler class to route these efficiently. Let's expand our bot with a welcome command, a help command, and a fun "roll dice" command:
import os
import random
from telegram import Update
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
BOT_TOKEN = os.environ.get("BOT_TOKEN")
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Send a welcome message when /start is issued."""
user = update.effective_user
welcome_text = (
f"Welcome, {user.first_name}! 👋\n"
"I'm your friendly helper bot.\n"
"Use /help to see what I can do."
)
await update.message.reply_text(welcome_text)
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""List all available commands."""
help_text = (
"📋 Available commands:\n"
"/start - Welcome message\n"
"/help - Show this help text\n"
"/roll - Roll a six-sided dice\n"
"/echo <text> - I'll repeat what you say\n"
"Just send any message and I'll echo it back!"
)
await update.message.reply_text(help_text)
async def roll_dice(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Roll a virtual dice and send the result."""
result = random.randint(1, 6)
await update.message.reply_text(f"🎲 You rolled a {result}!")
async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_message = update.message.text
await update.message.reply_text(f"You said: {user_message}")
def main():
app = Application.builder().token(BOT_TOKEN).build()
# Command handlers
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("help", help_command))
app.add_handler(CommandHandler("roll", roll_dice))
# Echo handler for all other text messages
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
print("Bot is running. Press Ctrl+C to stop.")
app.run_polling()
if __name__ == "__main__":
main()
Each CommandHandler listens for a specific command string. The order in which you add handlers does not affect command routing because CommandHandler checks the command prefix explicitly. For non-command text, the MessageHandler with filters.TEXT & ~filters.COMMAND catches everything else while excluding commands, preventing conflicts.
Inline Keyboards and Callback Queries
Inline keyboards are interactive buttons that appear below a bot message. When a user taps a button, Telegram sends a CallbackQuery update to your bot. This is perfect for polls, menus, confirmations, and navigation interfaces. Here's how to build a simple poll bot with inline buttons:
import os
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes
BOT_TOKEN = os.environ.get("BOT_TOKEN")
# Store poll votes in a simple dictionary (use a database in production)
poll_votes = {"yes": 0, "no": 0}
async def start_poll(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Send a poll question with inline keyboard buttons."""
keyboard = [
[
InlineKeyboardButton("👍 Yes", callback_data="poll_yes"),
InlineKeyboardButton("👎 No", callback_data="poll_no"),
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"Is python-telegram-bot the best Telegram library?",
reply_markup=reply_markup
)
async def handle_poll_answer(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Process the button press and update the vote count."""
query = update.callback_query
await query.answer() # Acknowledge the button press
choice = query.data # "poll_yes" or "poll_no"
if choice == "poll_yes":
poll_votes["yes"] += 1
elif choice == "poll_no":
poll_votes["no"] += 1
# Edit the original message to show updated results
yes_count = poll_votes["yes"]
no_count = poll_votes["no"]
total = yes_count + no_count
await query.edit_message_text(
f"📊 Current results:\n"
f"👍 Yes: {yes_count} ({yes_count/total*100:.1f}%)\n"
f"👎 No: {no_count} ({no_count/total*100:.1f}%)\n"
f"Total votes: {total}"
)
def main():
app = Application.builder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("poll", start_poll))
app.add_handler(CallbackQueryHandler(handle_poll_answer, pattern="^poll_"))
print("Poll bot is running. Press Ctrl+C to stop.")
app.run_polling()
if __name__ == "__main__":
main()
Key points about inline keyboards:
callback_datais a string payload attached to each button — keep it compact and structured- Always call
await query.answer()first to acknowledge the button press and dismiss the loading indicator on the user's client - Use
query.edit_message_text()to update the original message seamlessly without sending a new one - The
CallbackQueryHandlercan filter bypatternusing regex, allowing you to route different button presses to different handlers
Conversation Handler for Multi-Step Flows
Many bots need to guide users through a sequence of steps — collecting name, email, preferences, or processing an order form. The ConversationHandler manages stateful multi-step conversations with entry points, transition states, and fallback mechanisms. Here's a complete registration bot that collects a user's name and age in two steps:
import os
from telegram import Update, ReplyKeyboardMarkup, ReplyKeyboardRemove
from telegram.ext import (
Application,
CommandHandler,
ConversationHandler,
MessageHandler,
filters,
ContextTypes,
)
BOT_TOKEN = os.environ.get("BOT_TOKEN")
# Define conversation states as constants
NAME, AGE, CONFIRM = range(3)
async def start_registration(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Entry point: ask for the user's name."""
await update.message.reply_text(
"Let's get you registered! What is your full name?",
reply_markup=ReplyKeyboardRemove(),
)
return NAME
async def receive_name(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Store the name and ask for age."""
context.user_data["name"] = update.message.text
await update.message.reply_text(
f"Nice to meet you, {update.message.text}! How old are you?"
)
return AGE
async def receive_age(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Validate and store the age, then confirm."""
try:
age = int(update.message.text)
if age < 1 or age > 120:
await update.message.reply_text("Please enter a realistic age between 1 and 120.")
return AGE
context.user_data["age"] = age
except ValueError:
await update.message.reply_text("That doesn't look like a number. Please enter your age as a number.")
return AGE
name = context.user_data["name"]
age = context.user_data["age"]
await update.message.reply_text(
f"📋 Please confirm:\nName: {name}\nAge: {age}\n"
"Type /done to save or /cancel to discard.",
reply_markup=ReplyKeyboardMarkup([["/done", "/cancel"]], one_time_keyboard=True),
)
return CONFIRM
async def confirm_and_save(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Save the registration data and end the conversation."""
name = context.user_data.get("name")
age = context.user_data.get("age")
# In a real bot, you'd save this to a database
await update.message.reply_text(
f"✅ Registration complete! Welcome aboard, {name} ({age} years old).",
reply_markup=ReplyKeyboardRemove(),
)
return ConversationHandler.END
async def cancel_registration(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Cancel the conversation and clear collected data."""
context.user_data.clear()
await update.message.reply_text(
"❌ Registration cancelled. You can start again with /register.",
reply_markup=ReplyKeyboardRemove(),
)
return ConversationHandler.END
def main():
app = Application.builder().token(BOT_TOKEN).build()
conv_handler = ConversationHandler(
entry_points=[CommandHandler("register", start_registration)],
states={
NAME: [MessageHandler(filters.TEXT & ~filters.COMMAND, receive_name)],
AGE: [MessageHandler(filters.TEXT & ~filters.COMMAND, receive_age)],
CONFIRM: [
CommandHandler("done", confirm_and_save),
CommandHandler("cancel", cancel_registration),
],
},
fallbacks=[CommandHandler("cancel", cancel_registration)],
)
app.add_handler(conv_handler)
print("Registration bot is running. Press Ctrl+C to stop.")
app.run_polling()
if __name__ == "__main__":
main()
The ConversationHandler works as a finite state machine:
entry_pointsspecify which commands or handlers initiate the conversation- The
statesdictionary maps each state constant to a list of handlers that are active in that state fallbacksprovide escape hatches — handlers that work in any state to cancel or reset the conversation- Each handler function returns the next state to transition to, or
ConversationHandler.ENDto terminate context.user_datapersists data across all steps of the conversation for that specific user
Sending Photos, Documents, and Rich Media
Telegram bots can send more than just text. You can upload photos, documents, audio files, video, and even location pins. PTB provides straightforward methods for each. Here's an example that sends a local image and a document in response to commands:
import os
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes
BOT_TOKEN = os.environ.get("BOT_TOKEN")
async def send_photo(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Send a photo from a local file."""
photo_path = "assets/welcome_image.jpg"
caption = "Here's a welcome photo for you! 🌅"
# If the file doesn't exist, send an error message instead
try:
await update.message.reply_photo(
photo=open(photo_path, "rb"),
caption=caption,
)
except FileNotFoundError:
await update.message.reply_text("Sorry, the photo file is missing on the server.")
async def send_document(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Send a PDF document to the user."""
doc_path = "assets/info_guide.pdf"
try:
await update.message.reply_document(
document=open(doc_path, "rb"),
filename="Information_Guide.pdf",
caption="📄 Here's your requested document.",
)
except FileNotFoundError:
await update.message.reply_text("The document is currently unavailable.")
async def send_location(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Send a pinned location."""
latitude = 40.748817
longitude = -73.985428
await update.message.reply_location(
latitude=latitude,
longitude=longitude,
)
await update.message.reply_text("📍 This is the Empire State Building in NYC!")
def main():
app = Application.builder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("photo", send_photo))
app.add_handler(CommandHandler("document", send_document))
app.add_handler(CommandHandler("location", send_location))
print("Media bot is running. Press Ctrl+C to stop.")
app.run_polling()
if __name__ == "__main__":
main()
For production use, you might want to send files by URL or by file_id (a unique identifier Telegram assigns to every uploaded file, allowing reuse without re-uploading). After sending a file once, you can store its file_id from the returned message and use it indefinitely:
# After sending a photo, capture its file_id
sent_message = await update.message.reply_photo(photo=open("image.jpg", "rb"))
file_id = sent_message.photo[-1].file_id # The highest resolution version
# Later, send the same photo by file_id without re-uploading
await update.message.reply_photo(photo=file_id, caption="Reusing the same photo!")
Advanced Features and Best Practices
Error Handling and Logging
Robust bots must handle errors gracefully. PTB allows you to register a global error handler that catches exceptions from any handler in your application. Combined with Python's logging module, this gives you visibility into runtime issues:
import logging
from telegram import Update
from telegram.ext import Application, ContextTypes
# Configure logging
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
level=logging.INFO,
)
logger = logging.getLogger(__name__)
async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Log errors and notify the user if possible."""
logger.error(f"Exception while handling an update: {context.error}", exc_info=True)
# If we can respond to the user, send a friendly error message
if update and update.effective_chat:
await update.effective_chat.send_message(
"⚠️ Something went wrong processing your request. "
"Our team has been notified. Please try again later."
)
def main():
app = Application.builder().token(BOT_TOKEN).build()
# Register your handlers here...
# Register the global error handler
app.add_error_handler(error_handler)
app.run_polling()
Best practices for error handling include:
- Always register an error handler to prevent silent failures
- Log the full traceback using
exc_info=Truefor debugging - Check whether
update.effective_chatexists before responding — some errors occur before a chat context is available - Use
try/exceptblocks inside individual handlers for expected failure modes (e.g., file not found, invalid user input)
Job Queue for Scheduled Tasks
The JobQueue allows you to schedule recurring or one-time tasks. This is useful for sending daily reminders, cleaning up stale data, or triggering notifications at specific times. Here's how to set up a daily morning message and a one-time delayed response:
import os
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes
BOT_TOKEN = os.environ.get("BOT_TOKEN")
async def daily_morning_message(context: ContextTypes.DEFAULT_TYPE):
"""Send a morning greeting to a specific chat every day."""
chat_id = context.job.chat_id
await context.bot.send_message(
chat_id=chat_id,
text="☀️ Good morning! Hope you have a wonderful day ahead."
)
async def set_daily_reminder(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Schedule a daily job for the chat where this command was issued."""
chat_id = update.effective_chat.id
# Remove any existing daily job for this chat to avoid duplicates
existing_jobs = context.job_queue.get_jobs_by_name(str(chat_id))
for job in existing_jobs:
job.schedule_removal()
# Schedule a new daily job at 08:00 UTC
context.job_queue.run_daily(
daily_morning_message,
time=datetime.time(hour=8, minute=0, tzinfo=datetime.timezone.utc),
chat_id=chat_id,
name=str(chat_id), # Unique name for deduplication
)
await update.message.reply_text("✅ Daily morning reminder set for 08:00 UTC!")
async def remind_me_in(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Schedule a one-time reminder after a specified number of seconds."""
try:
seconds = int(context.args[0])
except (IndexError, ValueError):
await update.message.reply_text("Usage: /remindme <seconds>")
return
chat_id = update.effective_chat.id
context.job_queue.run_once(
lambda ctx: ctx.bot.send_message(
chat_id=chat_id,
text=f"⏰ Reminder! {seconds} seconds have passed."
),
when=seconds,
chat_id=chat_id,
)
await update.message.reply_text(f"Alright, I'll remind you in {seconds} seconds.")
def main():
app = Application.builder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("setdaily", set_daily_reminder))
app.add_handler(CommandHandler("remindme", remind_me_in))
print("Scheduler bot is running. Press Ctrl+C to stop.")
app.run_polling()
if __name__ == "__main__":
main()
Note that JobQueue requires datetime to be imported. Add import datetime at the top of your file. The job queue runs in the same process as your bot and persists as long as the application is running. For production deployments where you need durability across restarts, consider persisting job data to a database.
Working with User and Chat Data
python-telegram-bot provides three distinct data storage contexts attached to every handler invocation:
context.user_data: A dictionary scoped to a specific user across all chats — survives across different group conversations with the same usercontext.chat_data: A dictionary scoped to a specific chat — survives across different users in the same groupcontext.bot_data: A dictionary shared globally across the entire bot instance — useful for configuration or shared state
Here's an example that tracks per-user visit counts using context.user_data:
async def track_visits(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Increment and report a per-user visit counter."""
if "visit_count" not in context.user_data:
context.user_data["visit_count"] = 0
context.user_data["visit_count"] += 1
count = context.user_data["visit_count"]
await update.message.reply_text(
f"👋 You've interacted with me {count} time(s)!"
)
By default, these dictionaries live in memory and are lost when the bot restarts. For persistent storage, PTB supports custom persistence backends. You can implement BasePersistence to store data in Redis, PostgreSQL, or a simple JSON file. Here's a minimal example using a dictionary-based persistence (suitable for development):
from telegram.ext import BasePersistence
class DictPersistence(BasePersistence):
def __init__(self):
super().__init__()
self._user_data = {}
self._chat_data = {}
self._bot_data = {}
self._conversations = {}
async def get_user_data(self):
return self._user_data.copy()
async def get_chat_data(self):
return self._chat_data.copy()
async def get_bot_data(self):
return self._bot_data.copy()
async def get_conversations(self, name):
return self._conversations.get(name, {})
async def update_user_data(self, user_id, data):
self._user_data[user_id] = data
async def update_chat_data(self, chat_id, data):
self._chat_data[chat_id] = data
async def update_bot_data(self, data):
self._bot_data = data
async def update_conversations(self, name, conversations):
self._conversations[name] = conversations
# Pass the persistence instance to the Application builder
persistence = DictPersistence()
app = Application.builder().token(BOT_TOKEN).persistence(persistence).build()
Webhook Mode for Production
Polling works perfectly for development and low-traffic bots, but production deployments benefit from webhooks — Telegram pushes updates directly to your server via HTTP POST requests. This eliminates the constant polling overhead and scales better. Here's a production-ready webhook setup using aiohttp:
import os
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes
BOT_TOKEN = os.environ.get("BOT_TOKEN")
WEBHOOK_URL = os.environ.get("WEBHOOK_URL") # e.g., "https://mybot.example.com/webhook"
WEBHOOK_PATH = "/webhook"
HOST = "0.0.0.0"
PORT = 8443
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("Hello from a webhook-powered bot!")
def main():
app = Application.builder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("start", start))
# Run the webhook server
app.run_webhook(
listen=HOST,
port=PORT,
webhook_url=WEBHOOK_URL,
webhook_path=WEBHOOK_PATH,
# Optional: provide SSL certificate for HTTPS
# cert="path/to/cert.pem",
# key="path/to/key.pem",
)
print(f"Webhook server listening on {HOST}:{PORT}")
if __name__ == "__main__":
main()
For the webhook URL, you must use HTTPS with a valid SSL certificate (self-signed certificates are acceptable for Telegram bots). Common deployment platforms include a VPS with Nginx reverse proxy, Heroku, Render, or Google Cloud Run. When using a reverse proxy, set the port to an internal port and let Nginx handle SSL termination.
Best Practices Summary
- Keep handlers focused: Each handler should do one thing well. Split complex logic into separate functions or modules
- Validate all user input: Never trust raw message text — sanitize strings, validate numeric ranges, and escape special characters
- Use async/await correctly: All PTB handler functions are coroutines; avoid blocking the event loop with synchronous I/O or CPU-heavy operations
- Store secrets securely: Use environment variables or a secrets manager for bot tokens, API keys, and database credentials
- Implement rate limiting: For bots handling high traffic, add cooldowns to prevent abuse and stay within Telegram's rate limits (approximately 30 messages per second per bot)
- Log extensively: Record all significant events, errors, and user interactions for debugging and analytics
- Test thoroughly: Simulate edge cases — empty messages, extremely long input, special characters, and concurrent users
- Version your bot: Use Git for source control and maintain a changelog so users can see what's new
Conclusion
Building a Telegram bot with Python and python-telegram-bot is a remarkably streamlined process that scales from a ten-line echo bot to a full-featured application with inline keyboards, multi-step conversations, scheduled jobs, and persistent storage. The library's asynchronous architecture, comprehensive handler system, and built-in job queue give you everything needed to create responsive, production-grade bots. By following the patterns and best practices outlined in this tutorial — starting with a solid project structure, layering handlers thoughtfully, validating input rigorously, and deploying via webhooks with proper error logging — you can build bots that serve users reliably and delightfully. The Telegram platform's rich API, combined with PTB's elegant Python interface, opens up endless possibilities: from simple notification services to interactive games, e-commerce assistants, and AI-powered chat agents. Start with the echo bot, iterate on the examples provided here, and you'll have a sophisticated Telegram bot running in production before you know it.