How to Build a Telegram Bot with Python: a Working Aiogram 3 Example
Build your first Telegram bot with Python and Aiogram 3: configure a token, handle /start, add an inline button, and verify the local run.
By Shcoder, independent developer
To build a Telegram bot with Python, start with one small, testable flow: a /start command, a reply, and an inline button. In this guide, we will build that project with Aiogram 3, keep the token out of source code, and verify it in Telegram. This is a working foundation, not a production service: add a database, persistent state storage, monitoring, and deployment separately.
If you prefer a guided path with exercises, start the free Aiogram 3 workshop. For a deeper walkthrough of keyboards and FSM, read the Aiogram 3 tutorial.
1. Create a bot and its environment
Create a bot with @BotFather and copy its token to .env. Do not send the token in chat, put it in source code, or commit .env to Git.
The example uses Python 3.10+ and was checked with Aiogram 3.31.0. Pin dependency versions in your project and upgrade them deliberately.
mkdir telegram-bot
cd telegram-bot
python -m venv .venv
Activate the environment:
# macOS / Linux
source .venv/bin/activate
# Windows PowerShell
.\.venv\Scripts\Activate.ps1
Create requirements.txt:
aiogram==3.31.0
python-dotenv==1.1.1
Install the dependencies:
python -m pip install -r requirements.txt
Create .env:
BOT_TOKEN=paste_your_BotFather_token_here
Add a .gitignore so the secret and virtual environment are not committed:
.env
.venv/
__pycache__/
2. Add a handler and a button
Create bot.py:
import asyncio
import os
from aiogram import Bot, Dispatcher, F, Router
from aiogram.filters import CommandStart
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from dotenv import load_dotenv
router = Router(name="main")
def main_keyboard() -> InlineKeyboardMarkup:
keyboard = InlineKeyboardBuilder()
keyboard.button(text="About this bot", callback_data="about")
return keyboard.as_markup()
@router.message(CommandStart())
async def start(message: Message) -> None:
name = message.from_user.full_name if message.from_user else "there"
await message.answer(
f"Hi, {name}! The bot is running.",
reply_markup=main_keyboard(),
)
@router.callback_query(F.data == "about")
async def show_about(callback: CallbackQuery) -> None:
await callback.answer()
if isinstance(callback.message, Message):
await callback.message.edit_text(
"This is a minimal Aiogram 3 example. Next, add your own flow.",
reply_markup=main_keyboard(),
)
async def main() -> None:
load_dotenv()
token = os.getenv("BOT_TOKEN")
if not token:
raise RuntimeError("Add BOT_TOKEN to the .env file")
bot = Bot(token=token)
dispatcher = Dispatcher()
dispatcher.include_router(router)
await dispatcher.start_polling(
bot,
allowed_updates=dispatcher.resolve_used_update_types(),
)
if __name__ == "__main__":
asyncio.run(main())
Start the app:
python bot.py
Open your bot in a private chat, send /start, and press the button. Stop polling with Ctrl+C.
Dispatcher receives updates and Router groups handlers. CommandStart() selects the start command; F.data == "about" filters the button callback. Call callback.answer() for every callback, or Telegram clients keep showing a progress indicator. See the official docs for Dispatcher and routing, command filters, and callback answers.
3. If the bot does not respond
- Check that
.venvis active and dependencies were installed into it. - Make sure
BOT_TOKENis in.envand has no accidental spaces or quote characters. - Run only one polling process for a token. Multiple processes usually conflict while fetching updates.
- If you configured a webhook earlier, check the update delivery mode: polling and webhook cannot be used simultaneously for one bot.
- Read the traceback in the terminal. Do not log the full token or private message contents.
4. What production requires
A working local example is not automatically ready for real customers. Before publishing, consider:
- Storage. Save orders and leads in a database with migrations. FSM stores a temporary conversation step; it is not a replacement for the primary database. The default
MemoryStorageis cleared on restart. For state that should survive restarts, you can use Redis. See Aiogram FSM storage. - External dependencies. With Docker Compose, a database may start after the application. Use a healthcheck and
depends_on: condition: service_healthy, and plan for reconnects. Docker Compose documentation. - Reliable flows. Validate input, check user permissions, and handle repeated updates and external API errors. Payments and digital delivery should be idempotent.
- Operations. Add structured logs without secrets, process restarts, backups, and a post-deploy check.
- Tests. Test business logic separately from the Telegram API, including commands, FSM, errors, and retries.
For the next practical step, take the free Aiogram 3 course or continue with this Aiogram 3 tutorial on keyboards and FSM. The full Telegram bot course continues with databases, payments, Mini Apps, tests, and release workflows. See real examples in the portfolio.
related
next step
Build your first Telegram bot
The free Aiogram 3 workshop takes you from the first reply to FSM and a VPS deployment.
share
Need a custom Telegram bot?
I build bots, Mini Apps, payments and automation end to end.