Intermediate
Telegram bots are everywhere — they moderate group chats, send weather alerts, track crypto prices, manage to-do lists, and even run entire customer support workflows. If you have ever wanted to build your own bot that responds to commands, sends images, presents clickable buttons, or holds multi-step conversations, Python and the python-telegram-bot library make it surprisingly straightforward.
The python-telegram-bot library is the most popular Python wrapper for the Telegram Bot API. It handles all the low-level HTTP communication, provides clean handler classes for different message types, and supports both simple command-response patterns and complex multi-turn conversations. You will need Python 3.9 or later and a free Telegram account to follow along. Installation is a single pip install command, and creating a bot token through Telegram’s BotFather takes about two minutes.
In this article we will walk through every step of building a Telegram bot from scratch. We will start by creating a bot token with BotFather, then install the library and build a basic echo bot. From there we will cover command handlers, sending text and media, inline keyboard buttons, conversation handlers for multi-step flows, error handling, and finally a complete real-life project — a Personal Expense Tracker bot that stores expenses in a JSON file and can summarize your spending by category. By the end, you will have the skills to build and deploy any Telegram bot you can imagine.
Building a Telegram Bot in Python: Quick Example
Before we dive into the details, here is a minimal working bot that responds to the /start command and echoes back any text message the user sends. This gives you a working bot in under 15 lines of code.
# quick_bot.py
from telegram import Update
from telegram.ext import ApplicationBuilder, CommandHandler, MessageHandler, filters, ContextTypes
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("Hello! I am your Python bot. Send me any message and I will echo it back.")
async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(f"You said: {update.message.text}")
app = ApplicationBuilder().token("YOUR_BOT_TOKEN").build()
app.add_handler(CommandHandler("start", start))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
app.run_polling()
Output (in Telegram chat):
User: /start
Bot: Hello! I am your Python bot. Send me any message and I will echo it back.
User: Python is awesome!
Bot: You said: Python is awesome!
This tiny script creates a fully functional Telegram bot. The CommandHandler listens for the /start command and calls our start function. The MessageHandler with filters.TEXT catches every regular text message and echoes it back. The run_polling() method keeps the bot running, continuously checking Telegram’s servers for new messages.
Want to go deeper? Below we cover how to get your bot token, handle multiple commands, send photos and documents, create interactive button menus, build multi-step conversations, and put it all together in a real expense tracker project.
What Is a Telegram Bot and Why Build One?
A Telegram bot is a special account on Telegram that is controlled by software rather than a human. When someone sends a message to your bot, Telegram forwards that message to your server (or your script running locally via polling), your code processes it, and sends a response back through the Telegram Bot API. The user experience feels like chatting with a person, but behind the scenes it is your Python code making the decisions.
Bots are useful for automation, notifications, data collection, and interactive tools. Unlike building a full web application with a frontend, a Telegram bot gives you a polished chat interface for free — Telegram handles the UI, push notifications, media rendering, and cross-platform support. You just write the logic.
The python-telegram-bot library abstracts the raw HTTP API into a clean, Pythonic interface. Here is how its key concepts map to what you will be building:
| Concept | What It Does | Example Use |
|---|---|---|
Application | The main bot engine that manages handlers and polling | Starting and running the bot |
CommandHandler | Responds to slash commands like /start, /help | Greeting users, showing menus |
MessageHandler | Responds to regular messages (text, photos, etc.) | Echoing text, processing uploads |
CallbackQueryHandler | Responds to inline button presses | Interactive menus, confirmations |
ConversationHandler | Manages multi-step conversation flows | Forms, surveys, step-by-step input |
filters | Narrows which messages trigger a handler | Only text, only photos, only from groups |
Now that you understand the building blocks, let us set up BotFather and get a token so we can start coding.
Setting Up BotFather and Getting Your Bot Token
Every Telegram bot needs a unique token — a long string that authenticates your code with the Telegram API. You get this token by talking to BotFather, which is itself a Telegram bot that manages bot creation. This is a one-time setup that takes about two minutes.
Open Telegram and search for @BotFather. Start a conversation and send the /newbot command. BotFather will ask you for two things: a display name for your bot (anything you like, such as “My Python Bot”) and a username that must end in bot (such as my_python_tutorial_bot). Once you provide both, BotFather responds with your bot token.
# Your token will look something like this (this is a fake example):
# 7891234567:AAF_example-token-string-here-abc123
# IMPORTANT: Never share your real token publicly.
# Store it in an environment variable:
# In your terminal:
# export TELEGRAM_BOT_TOKEN="7891234567:AAF_your-real-token-here"
# In your Python code:
import os
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
The token is essentially the password to your bot. Anyone who has it can control your bot, so never commit it to a public repository or paste it in shared documents. The safest approach is to store it in an environment variable and read it with os.environ.get() as shown above. For development, you can also use a .env file with the python-dotenv library.

Installing python-telegram-bot
With your token ready, the next step is installing the library. The python-telegram-bot library is available on PyPI and supports Python 3.9 and above. A single pip command installs everything you need.
# install_check.py
# Install from terminal: pip install python-telegram-bot
# Verify the installation
import telegram
print(f"python-telegram-bot version: {telegram.__version__}")
Output:
python-telegram-bot version: 21.10
If the import works and prints a version number, you are ready to go. The library includes everything — HTTP handling, handler classes, inline keyboards, and conversation management. No additional packages are required for the tutorials in this article.
Handling Commands with CommandHandler
Commands are messages that start with a forward slash, like /start, /help, or /weather. They are the primary way users interact with bots. The CommandHandler class lets you register a Python function for each command your bot supports.
Let us build a bot that responds to three commands: /start for a welcome message, /help for a list of available commands, and /about for information about the bot.
# command_bot.py
import os
from telegram import Update
from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_name = update.effective_user.first_name
await update.message.reply_text(
f"Welcome, {user_name}! I am a demo bot.\n"
f"Use /help to see what I can do."
)
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
help_text = (
"Here are my commands:\n\n"
"/start - Start the bot\n"
"/help - Show this help message\n"
"/about - Learn about this bot"
)
await update.message.reply_text(help_text)
async def about(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(
"I was built with Python and python-telegram-bot v21.\n"
"Tutorial: pythonhowtoprogram.com"
)
app = ApplicationBuilder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("help", help_command))
app.add_handler(CommandHandler("about", about))
app.run_polling()
Output (in Telegram chat):
User: /start
Bot: Welcome, Alice! I am a demo bot.
Use /help to see what I can do.
User: /help
Bot: Here are my commands:
/start - Start the bot
/help - Show this help message
/about - Learn about this bot
Each CommandHandler takes two arguments: the command name (without the slash) and the async function to call. The update object contains everything about the incoming message — who sent it, the chat ID, the message text, and more. The context object provides access to bot-level data and the bot instance itself. Notice how we access the user’s first name with update.effective_user.first_name to personalize the greeting.
Sending Messages and Media
Text replies are just the beginning. Telegram bots can send photos, documents, audio files, locations, and formatted messages. The python-telegram-bot library provides dedicated methods for each media type, all accessible through the bot instance or the message reply methods.
Here is a bot that demonstrates sending a photo from a URL, a document, and a message with HTML formatting:
# media_bot.py
import os
from telegram import Update
from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
async def send_photo(update: Update, context: ContextTypes.DEFAULT_TYPE):
# Send a photo from a public URL
photo_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/800px-Python-logo-notext.svg.png"
await update.message.reply_photo(
photo=photo_url,
caption="The Python logo - beautiful, isn't it?"
)
async def send_formatted(update: Update, context: ContextTypes.DEFAULT_TYPE):
# Send a message with HTML formatting
formatted_text = (
"<b>Bold text</b>\n"
"<i>Italic text</i>\n"
"<code>inline_code()</code>\n\n"
"<pre># Code block\nprint('Hello from a code block!')</pre>"
)
await update.message.reply_text(formatted_text, parse_mode="HTML")
async def send_document(update: Update, context: ContextTypes.DEFAULT_TYPE):
# Create and send a text file on the fly
content = "This file was generated by your Telegram bot!"
file_bytes = content.encode("utf-8")
await update.message.reply_document(
document=file_bytes,
filename="bot_message.txt",
caption="Here is your generated document."
)
app = ApplicationBuilder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("photo", send_photo))
app.add_handler(CommandHandler("format", send_formatted))
app.add_handler(CommandHandler("doc", send_document))
app.run_polling()
Output (in Telegram chat):
User: /photo
Bot: [Sends Python logo image with caption "The Python logo - beautiful, isn't it?"]
User: /format
Bot: Bold text
Italic text
inline_code()
# Code block
print('Hello from a code block!')
User: /doc
Bot: [Sends bot_message.txt file with caption "Here is your generated document."]
The reply_photo() method accepts a URL or a file path. For formatted text, Telegram supports both HTML and Markdown — we use parse_mode="HTML" because it handles nested formatting more reliably. The reply_document() method can send any file, including ones you generate dynamically from bytes. This is incredibly useful for bots that create reports, export data, or generate files on demand.

Using Inline Keyboard Buttons for User Interaction
One of the most powerful features in Telegram bots is inline keyboards — rows of clickable buttons that appear directly below a message. These buttons can trigger actions, navigate menus, or collect user choices without the user typing anything. They make your bot feel like a polished application rather than a text-only chat.
Here is a bot that presents a menu with inline buttons and responds when the user clicks one:
# button_bot.py
import os
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import ApplicationBuilder, CommandHandler, CallbackQueryHandler, ContextTypes
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
async def menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
# Create a 2x2 grid of buttons
keyboard = [
[
InlineKeyboardButton("Python Basics", callback_data="basics"),
InlineKeyboardButton("Web Scraping", callback_data="scraping"),
],
[
InlineKeyboardButton("APIs", callback_data="apis"),
InlineKeyboardButton("Automation", callback_data="automation"),
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text("What topic interests you?", reply_markup=reply_markup)
async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer() # Acknowledge the button press
topics = {
"basics": "Python Basics: Start with variables, loops, and functions!",
"scraping": "Web Scraping: Use BeautifulSoup and Selenium to extract data.",
"apis": "APIs: Learn requests, REST APIs, and authentication.",
"automation": "Automation: Automate files, emails, and workflows.",
}
response = topics.get(query.data, "Unknown topic selected.")
await query.edit_message_text(text=f"Great choice!\n\n{response}")
app = ApplicationBuilder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("menu", menu))
app.add_handler(CallbackQueryHandler(button_callback))
app.run_polling()
Output (in Telegram chat):
User: /menu
Bot: What topic interests you?
[Python Basics] [Web Scraping]
[APIs] [Automation]
User: [clicks "Web Scraping"]
Bot: Great choice!
Web Scraping: Use BeautifulSoup and Selenium to extract data.
Each InlineKeyboardButton has a visible label and a callback_data string that gets sent to your bot when the user clicks it. The CallbackQueryHandler catches these clicks and routes them to your callback function. Always call query.answer() first to remove the loading spinner from the button — without this, Telegram shows a progress indicator that never goes away. Then use query.edit_message_text() to update the original message in place, which gives a smooth, app-like experience.
Building Multi-Step Conversations
Simple command-response bots are useful, but many real-world bots need to collect information across multiple messages — like a form where you ask for the user’s name, then their email, then their preference. The ConversationHandler manages these multi-step flows by tracking which “state” each user is in and routing their messages to the appropriate handler function.
Here is a bot that collects a user’s name, favorite programming language, and experience level through a guided conversation:
# conversation_bot.py
import os
from telegram import Update, ReplyKeyboardMarkup, ReplyKeyboardRemove
from telegram.ext import (
ApplicationBuilder, CommandHandler, MessageHandler,
ConversationHandler, ContextTypes, filters
)
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
# Define conversation states
NAME, LANGUAGE, EXPERIENCE = range(3)
async def start_survey(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(
"Welcome to the developer survey! What is your name?",
reply_markup=ReplyKeyboardRemove() # Remove any previous keyboard
)
return NAME # Move to the NAME state
async def get_name(update: Update, context: ContextTypes.DEFAULT_TYPE):
context.user_data["name"] = update.message.text
languages = [["Python", "JavaScript"], ["Go", "Rust"]]
await update.message.reply_text(
f"Nice to meet you, {update.message.text}! "
f"What is your favorite programming language?",
reply_markup=ReplyKeyboardMarkup(languages, one_time_keyboard=True)
)
return LANGUAGE # Move to the LANGUAGE state
async def get_language(update: Update, context: ContextTypes.DEFAULT_TYPE):
context.user_data["language"] = update.message.text
levels = [["Beginner", "Intermediate", "Advanced"]]
await update.message.reply_text(
f"Great choice! What is your experience level?",
reply_markup=ReplyKeyboardMarkup(levels, one_time_keyboard=True)
)
return EXPERIENCE # Move to the EXPERIENCE state
async def get_experience(update: Update, context: ContextTypes.DEFAULT_TYPE):
context.user_data["experience"] = update.message.text
# Summarize the collected data
data = context.user_data
summary = (
f"Survey complete! Here is your profile:\n\n"
f"Name: {data['name']}\n"
f"Language: {data['language']}\n"
f"Experience: {data['experience']}\n\n"
f"Thanks for participating!"
)
await update.message.reply_text(summary, reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END # End the conversation
async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("Survey cancelled.", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
# Build the conversation handler
survey_handler = ConversationHandler(
entry_points=[CommandHandler("survey", start_survey)],
states={
NAME: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_name)],
LANGUAGE: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_language)],
EXPERIENCE: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_experience)],
},
fallbacks=[CommandHandler("cancel", cancel)],
)
app = ApplicationBuilder().token(BOT_TOKEN).build()
app.add_handler(survey_handler)
app.run_polling()
Output (in Telegram chat):
User: /survey
Bot: Welcome to the developer survey! What is your name?
User: Alice
Bot: Nice to meet you, Alice! What is your favorite programming language?
[Python] [JavaScript]
[Go] [Rust]
User: Python
Bot: Great choice! What is your experience level?
[Beginner] [Intermediate] [Advanced]
User: Intermediate
Bot: Survey complete! Here is your profile:
Name: Alice
Language: Python
Experience: Intermediate
Thanks for participating!
The ConversationHandler is the most complex handler in the library, but the pattern is straightforward once you understand it. You define numbered states (we used range(3) to create NAME=0, LANGUAGE=1, EXPERIENCE=2). Each handler function returns the next state to transition to. The context.user_data dictionary persists across the conversation, letting you accumulate responses step by step. The ReplyKeyboardMarkup shows a custom keyboard with predefined choices, which reduces typos and makes the experience smoother. Always include a /cancel fallback so users can exit the conversation at any point.

Error Handling and Logging
Production bots need to handle errors gracefully. Network timeouts, invalid user input, and API rate limits can all cause exceptions. The python-telegram-bot library provides a built-in error handler that catches any uncaught exception from your handlers, so your bot keeps running even when something goes wrong.
# error_handling_bot.py
import os
import logging
from telegram import Update
from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes
# Set up logging to see what is happening
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
level=logging.INFO
)
logger = logging.getLogger(__name__)
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
async def risky_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
# This will raise an error if the user doesn't provide a number
user_input = context.args[0] if context.args else None
if user_input is None:
await update.message.reply_text("Please provide a number: /divide 10")
return
result = 100 / int(user_input) # Could raise ValueError or ZeroDivisionError
await update.message.reply_text(f"100 / {user_input} = {result}")
async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE):
logger.error(f"Exception while handling an update: {context.error}")
if update and hasattr(update, "message") and update.message:
await update.message.reply_text(
"Something went wrong. Please try again with valid input."
)
app = ApplicationBuilder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("divide", risky_command))
app.add_error_handler(error_handler)
app.run_polling()
Output (in Telegram chat):
User: /divide 5
Bot: 100 / 5 = 20.0
User: /divide 0
Bot: Something went wrong. Please try again with valid input.
User: /divide abc
Bot: Something went wrong. Please try again with valid input.
The add_error_handler() method registers a function that catches any unhandled exception from any handler in your bot. The context.error attribute contains the actual exception. We log it for debugging and send a friendly message to the user. The logging module is configured at the top to output timestamped messages — this is essential for debugging issues in production. In the real world, you would also want to log the full traceback and possibly send error notifications to yourself via a separate Telegram message.
Real-Life Example: Personal Expense Tracker Bot

Now let us put everything together into a practical project. This expense tracker bot lets users add expenses with a category and amount, view their spending by category, and clear their data. It stores everything in a JSON file so expenses persist between bot restarts. This project uses command handlers, formatted messages, context storage, and file I/O — all the concepts we covered in this article.
# expense_tracker_bot.py
import os
import json
from datetime import datetime
from telegram import Update
from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
DATA_FILE = "expenses.json"
def load_expenses():
"""Load expenses from JSON file, return empty dict if file missing."""
if os.path.exists(DATA_FILE):
with open(DATA_FILE, "r") as f:
return json.load(f)
return {}
def save_expenses(data):
"""Save expenses dictionary to JSON file."""
with open(DATA_FILE, "w") as f:
json.dump(data, f, indent=2)
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(
"Welcome to Expense Tracker!\n\n"
"Commands:\n"
"/add <category> <amount> - Add an expense\n"
"/summary - View spending by category\n"
"/history - View recent expenses\n"
"/clear - Clear all expenses"
)
async def add_expense(update: Update, context: ContextTypes.DEFAULT_TYPE):
if len(context.args) < 2:
await update.message.reply_text("Usage: /add food 12.50")
return
category = context.args[0].lower()
try:
amount = float(context.args[1])
except ValueError:
await update.message.reply_text("Amount must be a number. Example: /add food 12.50")
return
user_id = str(update.effective_user.id)
expenses = load_expenses()
if user_id not in expenses:
expenses[user_id] = []
expenses[user_id].append({
"category": category,
"amount": amount,
"date": datetime.now().strftime("%Y-%m-%d %H:%M")
})
save_expenses(expenses)
total = sum(e["amount"] for e in expenses[user_id] if e["category"] == category)
await update.message.reply_text(
f"Added ${amount:.2f} to {category}.\n"
f"Total in {category}: ${total:.2f}"
)
async def summary(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = str(update.effective_user.id)
expenses = load_expenses()
user_expenses = expenses.get(user_id, [])
if not user_expenses:
await update.message.reply_text("No expenses yet. Use /add category amount.")
return
# Group by category
categories = {}
for expense in user_expenses:
cat = expense["category"]
categories[cat] = categories.get(cat, 0) + expense["amount"]
grand_total = sum(categories.values())
lines = [f" {cat}: ${amt:.2f}" for cat, amt in sorted(categories.items())]
text = f"Spending Summary:\n\n" + "\n".join(lines) + f"\n\nTotal: ${grand_total:.2f}"
await update.message.reply_text(text)
async def history(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = str(update.effective_user.id)
expenses = load_expenses()
user_expenses = expenses.get(user_id, [])
if not user_expenses:
await update.message.reply_text("No expenses yet.")
return
recent = user_expenses[-10:] # Show last 10 expenses
lines = [f" {e['date']} | {e['category']}: ${e['amount']:.2f}" for e in recent]
text = "Recent Expenses:\n\n" + "\n".join(lines)
await update.message.reply_text(text)
async def clear(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = str(update.effective_user.id)
expenses = load_expenses()
expenses[user_id] = []
save_expenses(expenses)
await update.message.reply_text("All expenses cleared.")
app = ApplicationBuilder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("add", add_expense))
app.add_handler(CommandHandler("summary", summary))
app.add_handler(CommandHandler("history", history))
app.add_handler(CommandHandler("clear", clear))
app.run_polling()
Output (in Telegram chat):
User: /start
Bot: Welcome to Expense Tracker!
Commands:
/add <category> <amount> - Add an expense
/summary - View spending by category
/history - View recent expenses
/clear - Clear all expenses
User: /add food 12.50
Bot: Added $12.50 to food.
Total in food: $12.50
User: /add transport 35.00
Bot: Added $35.00 to transport.
Total in transport: $35.00
User: /add food 8.75
Bot: Added $8.75 to food.
Total in food: $21.25
User: /summary
Bot: Spending Summary:
food: $21.25
transport: $35.00
Total: $56.25
User: /history
Bot: Recent Expenses:
2026-03-13 14:30 | food: $12.50
2026-03-13 14:31 | transport: $35.00
2026-03-13 14:32 | food: $8.75
This project ties together almost every concept from the article. It uses CommandHandler for five different commands, context.args to parse user input, JSON file I/O for persistence, input validation with helpful error messages, and formatted text output. The expenses are stored per user (keyed by Telegram user ID), so multiple people can use the same bot without their data mixing together. You could extend this by adding inline buttons for quick category selection, a /export command that sends a CSV file, or monthly budget limits with alerts.
Frequently Asked Questions
What is the difference between polling and webhooks?
Polling means your bot continuously asks Telegram "any new messages?" in a loop — this is what run_polling() does and it works great for development and small bots. Webhooks are the opposite: you give Telegram a URL, and Telegram sends new messages directly to your server. Webhooks are more efficient for production bots because they eliminate the constant polling overhead. For most tutorials and personal projects, polling is simpler and works perfectly fine.
How do I keep my bot running 24/7?
During development, running the script on your local machine is fine, but it stops when you close the terminal. For production, deploy your bot to a cloud server. Popular free or low-cost options include Railway, Render, PythonAnywhere, and a small VPS from providers like DigitalOcean. You can also use a Raspberry Pi at home. The key is that the Python process needs to stay running continuously — tools like systemd, screen, or pm2 can manage this for you.
Can I run multiple bots from one Python script?
Yes, but it is generally better to run each bot as a separate script or process. Each Application instance manages its own polling loop and handlers, and mixing them in one script can make debugging harder. If you need bots to share data, use a shared database or file rather than trying to run them in the same process.
Does Telegram have rate limits for bots?
Yes. Telegram limits bots to about 30 messages per second overall, and 1 message per second to the same chat. If your bot sends too many messages too quickly, Telegram returns a 429 error with a retry_after value telling you how long to wait. The python-telegram-bot library handles basic rate limiting automatically, but for high-volume bots you should implement message queuing and respect the limits explicitly.
How do I make my bot work in group chats?
By default, bots in groups only see messages that start with a slash command or explicitly mention the bot. To see all messages, you need to disable "Group Privacy" mode in BotFather using the /setprivacy command. In your code, you can filter group messages using filters.ChatType.GROUP or filters.ChatType.SUPERGROUP to create group-specific behavior.
What is the best way to store data for a Telegram bot?
For simple bots with small amounts of data, JSON files work well (as we used in the expense tracker). For anything more complex, use SQLite (built into Python via the sqlite3 module) for structured data without needing an external database server. For production bots with many users, PostgreSQL or MongoDB are popular choices. The python-telegram-bot library also has built-in persistence classes that can automatically save conversation state and user data.
Conclusion
You now have a solid foundation for building Telegram bots with Python. We covered the entire workflow: setting up a bot through BotFather, installing python-telegram-bot, handling commands with CommandHandler, sending text, photos, and documents, creating interactive inline keyboard buttons with CallbackQueryHandler, building multi-step conversations with ConversationHandler, implementing error handling and logging, and tying it all together with a Personal Expense Tracker project.
The expense tracker is a great starting point for your own projects. Try extending it with inline buttons for quick category selection, a monthly budget limit with warnings, or a /export command that generates a CSV report of your spending. The patterns you learned here — handlers, filters, context data, and conversation states — apply to any bot you want to build, from a simple notification bot to a full customer service assistant.
For the complete API reference, advanced topics like job queues and custom filters, and deployment guides, check out the official python-telegram-bot documentation and the Telegram Bot API reference.