1. Create the bot in BotFather
- Open @BotFather in Telegram and send
/newbot. - Choose a display name and a username ending in
bot. - Copy the token. Treat it like a password: anyone with it controls the bot. See bot security.
While you are there, /setcommands sets the command menu, or you can set it from code later (step 5). The Telegram bots guide explains the platform concepts used here.
2. Set up the project
python-telegram-bot (PTB) requires Python 3.10 or newer at the time of writing; version 22.8 was the latest release when this guide was written.
mkdir mybot && cd mybot
python3 -m venv .venv && . .venv/bin/activate
pip install "python-telegram-bot[job-queue]==22.*"
echo 'BOT_TOKEN=123456:ABC...' > .env # never commit this file
echo '.env' >> .gitignore
The job-queue extra adds scheduling (step 7). Add webhooks too if you will use a webhook: python-telegram-bot[job-queue,webhooks].
3. A first working bot
# bot.py
import logging, os
from telegram import Update
from telegram.ext import Application, CommandHandler, MessageHandler, ContextTypes, filters
logging.basicConfig(format="%(asctime)s %(levelname)s %(name)s: %(message)s", level=logging.INFO)
logging.getLogger("httpx").setLevel(logging.WARNING) # httpx logs every request URL, which contains the token
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(f"Hi {update.effective_user.first_name}! Send me any text.")
async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(update.message.text)
def main():
app = Application.builder().token(os.environ["BOT_TOKEN"]).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
app.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()
Run it with the token loaded (set -a; . ./.env; set +a; python bot.py) and send /start to your bot.
Notes:
- Handlers are checked in order within a group; the first match handles the update.
filters.TEXT & ~filters.COMMANDmatches text that is not a command, so/startis not echoed.- Silencing the
httpxlogger matters: at INFO level it prints request URLs, and Telegram URLs contain the token.
4. Conversations with ConversationHandler
Multi-step flows (ask a name, then an email) are a state machine. PTB's ConversationHandler maps states to handlers:
from telegram.ext import ConversationHandler
NAME, CITY = range(2)
async def signup(update, context):
await update.message.reply_text("What is your name? (/cancel to stop)")
return NAME
async def got_name(update, context):
context.user_data["name"] = update.message.text.strip()[:64]
await update.message.reply_text("Which city are you in?")
return CITY
async def got_city(update, context):
name = context.user_data["name"]
await update.message.reply_text(f"Thanks {name}, saved {update.message.text.strip()[:64]}.")
return ConversationHandler.END
async def cancel(update, context):
await update.message.reply_text("Cancelled.")
return ConversationHandler.END
conv = ConversationHandler(
entry_points=[CommandHandler("signup", signup)],
states={NAME: [MessageHandler(filters.TEXT & ~filters.COMMAND, got_name)],
CITY: [MessageHandler(filters.TEXT & ~filters.COMMAND, got_city)]},
fallbacks=[CommandHandler("cancel", cancel)],
conversation_timeout=600,
)
app.add_handler(conv) # add before the echo handler
Sketch the states first in the flow designer; it is much easier than debugging a tangled conversation.
5. Command menu and inline buttons
Set the command menu at startup with a post_init hook:
from telegram import BotCommand
async def post_init(app):
await app.bot.set_my_commands([
BotCommand("start", "Start the bot"),
BotCommand("signup", "Register"),
BotCommand("cancel", "Cancel the current action"),
])
app = Application.builder().token(os.environ["BOT_TOKEN"]).post_init(post_init).build()
For buttons under a message, use InlineKeyboardMarkup and a CallbackQueryHandler, and always call query.answer(). Our post on inline keyboards and callback queries has a complete example, and the keyboard builder designs the layout.
6. Error handling and persistence
Register one error handler so exceptions are logged instead of disappearing:
async def on_error(update, context):
logging.exception("Update caused an error", exc_info=context.error)
app.add_error_handler(on_error)
context.user_data and context.chat_data live in memory and vanish on restart. For small bots, PicklePersistence stores them in a file:
from telegram.ext import PicklePersistence
persistence = PicklePersistence(filepath="data/bot.pickle")
app = Application.builder().token(TOKEN).persistence(persistence).build()
Mark conversations with persistent=True, name="signup" to keep their state too. For anything bigger, use a real database; see databases and state for bots.
7. Scheduled jobs
With the job-queue extra, context.job_queue schedules callbacks:
async def remind(context):
await context.bot.send_message(context.job.chat_id, f"Reminder: {context.job.data}")
async def remind_cmd(update, context):
minutes = int(context.args[0]); text = " ".join(context.args[1:]) or "time is up"
context.job_queue.run_once(remind, when=minutes * 60, chat_id=update.effective_chat.id, data=text)
await update.message.reply_text(f"I will remind you in {minutes} min.")
Jobs live in memory, so they are lost on restart. Durable reminders need storage; see scheduled messages and reminders.
8. Deploy: polling or webhook
Polling (simplest): run python bot.py as an always-on process on a VPS or a worker service. run_polling handles SIGTERM for clean restarts. Containerize it with this Docker setup.
Webhook: needs a public HTTPS URL, but lets the host scale and sleep between requests.
app.run_webhook(
listen="0.0.0.0",
port=int(os.environ.get("PORT", 8080)),
url_path="telegram",
webhook_url="https://your-app.example.com/telegram",
secret_token=os.environ["WEBHOOK_SECRET"],
)
run_webhook registers the webhook with Telegram and checks the secret header for you. Background in Telegram setWebhook step by step; hosting options in best hosting for Telegram bots and deploying a Python bot.
Checklist before going live
- Token in an environment variable,
httpxlogging at WARNING. - Error handler registered; logs visible on the host.
- Persistence or a database for anything users expect to survive a restart.
- One running instance for polling.
- Respect Telegram's sending limits for broadcasts; see rate limits explained.
Sources
- python-telegram-bot documentation: docs.python-telegram-bot.org
- python-telegram-bot wiki and examples: github.com/python-telegram-bot/python-telegram-bot/wiki
- Telegram Bot API reference: core.telegram.org/bots/api
- Telegram, BotFather and bot features: core.telegram.org/bots/features
Related comparisons and tools
Frequently asked questions
python-telegram-bot or aiogram?
Both are solid async Python libraries. python-telegram-bot has extensive docs and built-in conversation and job helpers; aiogram is built on aiohttp with a router and middleware design. Pick the one whose docs you find clearer.
Why does my bot stop answering after I deploy a second copy?
Only one process may call getUpdates for a token. Two polling instances conflict and Telegram returns 409 Conflict to one of them. Run a single instance or switch to a webhook.