Build a Telegram Bot with Python

Published on: July 10, 2026
Reading time: 5 minutes
Criação de bot Telegram com Python

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

  1. Open Telegram and start a conversation with the verified @BotFather account.
  2. Send /newbot.
  3. Choose a display name.
  4. Choose a username ending in bot.
  5. 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 .venv

Activate it and install the library:

python -m pip install python-telegram-bot python-dotenv

Check 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_token

Add .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.txt

Save dependencies with:

python -m pip freeze > requirements.txt

Separating 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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    representação de automação com ícones de engrenagens, gráficos e pessoas no background
    Automation and Scripts
    Foto de perfil de Leandro Hirt da Academify

    Automate Everyday Tasks with Python

    Learn how to automate everyday tasks with Python: organize files, rename batches, process CSV data, create reports, schedule scripts, and

    Ler mais

    Tempo de leitura: 5 minutos
    10/07/2026
    logo do BeautifulSoup em um fundo branco
    Automation and Scripts
    Foto de perfil de Leandro Hirt da Academify

    Web Scraping with BeautifulSoup and Requests

    Learn web scraping with Requests and BeautifulSoup: fetch pages, parse HTML, select elements, clean data, follow pagination, handle errors, respect

    Ler mais

    Tempo de leitura: 6 minutos
    10/07/2026
    Automação web com Selenium usando Python
    Automation and ScriptsWeb Development
    Foto de perfil de Leandro Hirt da Academify

    Selenium with Python: Complete Web Automation Guide

    Learn Selenium with Python from setup to browser control, element locators, waits, forms, screenshots, page objects, testing, and reliable web

    Ler mais

    Tempo de leitura: 5 minutos
    10/07/2026
    Geração de arquivos PDF usando biblioteca FPDF em Python
    Automation and Scripts
    Foto de perfil de Leandro Hirt da Academify

    Create PDF Files with Python and FPDF

    Learn to create PDF files with Python and fpdf2: pages, fonts, paragraphs, images, tables, headers, footers, Unicode, invoices, paths, and

    Ler mais

    Tempo de leitura: 4 minutos
    10/07/2026
    Logos do Python e Excel lado a lado representando a importação de dados do Excel para Python.
    Automation and Scripts
    Foto de perfil de Leandro Hirt da Academify

    Import Excel Data into Python with Pandas

    Learn how to import Excel data into Python with Pandas and openpyxl, select sheets and columns, clean values, handle dates

    Ler mais

    Tempo de leitura: 4 minutos
    10/07/2026
    Python e ícone de e-mail sobre teclado de notebook
    Automation and Scripts
    Foto de perfil de Leandro Hirt da Academify

    Automate Emails with Python: Complete Guide

    Learn how to automate emails with Python using EmailMessage and smtplib, environment variables, HTML, attachments, multiple recipients, scheduling, and error

    Ler mais

    Tempo de leitura: 5 minutos
    10/07/2026