A Telegram bot with Python can answer commands, collect information, send notifications, and automate repetitive conversations. Telegram provides the messaging platform and Bot API, while Python supplies the program logic. You can begin with a small command bot and later add buttons, databases, external APIs, scheduled messages, and deployment.
This guide uses the popular python-telegram-bot library and its asynchronous interface. You will create a bot with BotFather, protect the token, handle commands and text, add inline buttons, store simple user data, and prepare the project for reliable execution.
How Telegram bots work
A Telegram bot is a special account controlled through the Telegram Bot API. Users communicate with it in a private chat or group. Telegram sends each message or interaction to your application as an update, and your Python code decides how to respond.
Two common delivery methods are:
- Long polling: the program repeatedly asks Telegram for new updates. It is simple for development and small deployments.
- Webhook: Telegram sends updates to a public HTTPS endpoint. This is common in production web applications.
The examples below use long polling because it requires no public server.
Create a bot with BotFather
- Open Telegram and start a conversation with the verified @BotFather account.
- Send
/newbot. - Choose a display name.
- Choose a username ending in
bot. - Copy the token generated by BotFather.
The token grants control over the bot. Do not publish it in an article, repository, screenshot, or shared chat. If it is exposed, revoke it through BotFather and generate a replacement.
Create the project environment
Use a separate environment for the project. The Academify Python virtual environment guide explains why this prevents dependency conflicts:
python -m venv .venvActivate it and install the library:
python -m pip install python-telegram-bot python-dotenvCheck the package documentation for version-specific details at the official python-telegram-bot documentation.
Store the token in an environment file
Create a file named .env:
TELEGRAM_BOT_TOKEN=replace_with_your_real_tokenAdd .env to .gitignore:
.env
.venv/
__pycache__/The script can load the value without embedding it in source code:
import os
from dotenv import load_dotenv
load_dotenv()
TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
if not TOKEN:
raise RuntimeError("TELEGRAM_BOT_TOKEN is not configured")Raising a clear configuration error is better than allowing the application to fail later with an unclear authentication message. See the Python exception handling guide for related patterns.
Create the first working bot
import os
from dotenv import load_dotenv
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes
load_dotenv()
TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
user = update.effective_user
await update.message.reply_text(
f"Hello, {user.first_name}! Send /help to see the commands."
)
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.message.reply_text(
"/start - welcome message\n"
"/help - command list\n"
"/about - information about this bot"
)
def main() -> None:
if not TOKEN:
raise RuntimeError("Missing TELEGRAM_BOT_TOKEN")
app = Application.builder().token(TOKEN).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("help", help_command))
app.run_polling()
if __name__ == "__main__":
main()Run the script, open the bot in Telegram, and send /start. The handler is asynchronous, so the function uses async def and await. Readers unfamiliar with this style can consult the Python asyncio guide.
Add another command
async def about(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.message.reply_text(
"This bot was built with Python and the Telegram Bot API."
)
# In main():
app.add_handler(CommandHandler("about", about))Keep command functions small. If a command performs database work or calls an external service, place that logic in a separate module and let the handler coordinate the input and response.
Respond to normal text messages
A MessageHandler can process text that is not a command:
from telegram.ext import MessageHandler, filters
async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
text = update.message.text
await update.message.reply_text(f"You wrote: {text}")
# Add after command handlers:
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))Handler order matters. More specific handlers should normally be registered before broad text handlers.
Normalize input and create simple intent rules
For a small conversational bot, normalize the message before comparing it:
async def conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
text = update.message.text.strip().lower()
if text in {"hello", "hi", "hey"}:
reply = "Hello! How can I help?"
elif "hours" in text:
reply = "Support is available Monday through Friday."
elif "price" in text:
reply = "Tell me which plan you want to know about."
else:
reply = "I did not understand. Send /help to see the options."
await update.message.reply_text(reply)Rules work well for predictable questions. As the number of intents grows, store responses in structured data or split them into functions. The Python collections guide explains dictionaries and sets that help organize these rules.
Add inline buttons
Inline keyboards make a bot easier to use because users choose a known option instead of typing it:
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import CallbackQueryHandler
async def menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
keyboard = [
[InlineKeyboardButton("Courses", callback_data="courses")],
[InlineKeyboardButton("Support", callback_data="support")],
]
await update.message.reply_text(
"Choose an option:",
reply_markup=InlineKeyboardMarkup(keyboard),
)
async def button_click(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
query = update.callback_query
await query.answer()
if query.data == "courses":
text = "Visit the course catalog to see the available classes."
else:
text = "Describe your question and our support flow will begin."
await query.edit_message_text(text=text)
# In main():
app.add_handler(CommandHandler("menu", menu))
app.add_handler(CallbackQueryHandler(button_click))Always call query.answer(); otherwise, Telegram can leave a loading indicator visible to the user.
Read command arguments
Users can send arguments after a command, such as /add 10 25:
async def add_numbers(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
try:
numbers = [float(value) for value in context.args]
except ValueError:
await update.message.reply_text("Use numbers only, for example: /add 10 25")
return
if not numbers:
await update.message.reply_text("Provide at least one number.")
return
await update.message.reply_text(f"Total: {sum(numbers):g}")This example validates input before calculating. Validation should explain how the user can correct the request instead of returning a raw traceback.
Store per-user data
context.user_data keeps temporary data associated with a user during the process lifetime:
async def set_name(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if not context.args:
await update.message.reply_text("Use /name followed by your name.")
return
name = " ".join(context.args).strip()
context.user_data["name"] = name
await update.message.reply_text(f"Saved: {name}")This data is not automatically durable after a restart unless persistence is configured. For production, use an appropriate database and define retention, privacy, and deletion rules.
Send files and photos
from pathlib import Path
async def send_report(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
path = Path("reports/monthly.pdf")
if not path.exists():
await update.message.reply_text("The report is not available yet.")
return
with path.open("rb") as document:
await update.message.reply_document(document=document)Use the pathlib guide to manage files safely across operating systems. Validate size, type, and access permissions before sending user-provided files.
Add structured logging
import logging
logging.basicConfig(
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
level=logging.INFO,
)
logger = logging.getLogger(__name__)Log operational events and exceptions, but never print the bot token, passwords, private messages, or unnecessary personal information. The Python logging guide covers levels, files, handlers, and rotation.
Handle unexpected errors
async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
logger.exception("Unhandled Telegram update error", exc_info=context.error)
# In main():
app.add_error_handler(error_handler)An error handler records failures that escape individual commands. For expected errors, catch specific exception types close to the operation and send a useful response.
Organize a growing bot
A maintainable project may use this structure:
telegram_bot/
├── bot.py
├── handlers/
│ ├── commands.py
│ └── messages.py
├── services/
│ └── catalog.py
├── tests/
├── .env
├── .gitignore
└── requirements.txtSave dependencies with:
python -m pip freeze > requirements.txtSeparating handlers from services makes it possible to test business rules without connecting to Telegram.
Test the logic
Extract calculations and text classification into ordinary functions:
def classify_message(text: str) -> str:
normalized = text.strip().lower()
if "price" in normalized:
return "pricing"
if "help" in normalized:
return "support"
return "unknown"Then test those functions with Pytest. The Academify Pytest tutorial explains assertions, test organization, and fixtures.
Deploy the bot safely
A bot must keep running to receive updates. Common options include a virtual private server, a container platform, or a managed application service. Whichever option you choose:
- store the token in the platform’s secret manager or environment settings;
- restart the process automatically after failure;
- collect logs without exposing private content;
- pin and update dependencies deliberately;
- use a database for durable state;
- monitor errors and response time.
Do not run multiple long-polling instances with the same token unless the library and deployment design explicitly support it. Competing instances can consume updates unpredictably.
Common problems
- Unauthorized: the token is missing, malformed, revoked, or copied with extra characters.
- The bot does not answer: confirm the process is running, the handler is registered, and no broader handler is intercepting the update first.
- Commands work privately but not in groups: review BotFather privacy settings and group permissions.
- Old synchronous examples fail: verify that the tutorial matches the installed major version of
python-telegram-bot. - State disappears after restart: configure persistence or store important data in a database.
Conclusion
A Telegram bot with Python begins with a token, an Application, and a few handlers. From there, commands, message filters, inline buttons, files, persistence, and external services can turn the project into a practical automation tool. Keep credentials out of source code, validate every user input, separate handlers from business logic, and add logging and tests before deploying the bot for real users.






