1. Crea el bot en BotFather
- Abre @BotFather en Telegram y envía
/newbot. - Elige un nombre visible y un nombre de usuario que termine en
bot. - Copia el token. Trátalo como una contraseña: quien lo tenga controla el bot. Mira seguridad para bots.
Ya ahí, /setcommands define el menú de comandos, o puedes definirlo desde el código (paso 5). La guía de bots de Telegram explica los conceptos de la plataforma que usamos aquí.
2. Prepara el proyecto
python-telegram-bot (PTB) requiere Python 3.10 o más reciente al momento de escribir; la versión 22.8 era la más reciente cuando se escribió esta guía.
mkdir mibot && cd mibot
python3 -m venv .venv && . .venv/bin/activate
pip install "python-telegram-bot[job-queue]==22.*"
echo 'BOT_TOKEN=123456:ABC...' > .env # nunca subas este archivo
echo '.env' >> .gitignore
El extra job-queue agrega tareas programadas (paso 7). Agrega también webhooks si usarás webhook: python-telegram-bot[job-queue,webhooks].
3. Un primer bot que funciona
# 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 registra cada URL, y la URL contiene el token
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(f"¡Hola {update.effective_user.first_name}! Envíame un texto.")
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()
Ejecútalo con el token cargado (set -a; . ./.env; set +a; python bot.py) y envía /start a tu bot.
Notas:
- Los handlers se revisan en orden dentro de un grupo; el primero que coincide maneja el update.
filters.TEXT & ~filters.COMMANDcoincide con texto que no es comando, así que/startno se repite.- Silenciar el logger de
httpximporta: en nivel INFO imprime las URLs, y las URLs de Telegram contienen el token.
4. Conversaciones con ConversationHandler
Los flujos de varios pasos (pedir un nombre y luego una ciudad) son una máquina de estados. ConversationHandler asocia estados con handlers:
from telegram.ext import ConversationHandler
NOMBRE, CIUDAD = range(2)
async def registro(update, context):
await update.message.reply_text("¿Cómo te llamas? (/cancel para salir)")
return NOMBRE
async def con_nombre(update, context):
context.user_data["nombre"] = update.message.text.strip()[:64]
await update.message.reply_text("¿En qué ciudad estás?")
return CIUDAD
async def con_ciudad(update, context):
nombre = context.user_data["nombre"]
await update.message.reply_text(f"Gracias {nombre}, guardé {update.message.text.strip()[:64]}.")
return ConversationHandler.END
async def cancel(update, context):
await update.message.reply_text("Cancelado.")
return ConversationHandler.END
conv = ConversationHandler(
entry_points=[CommandHandler("registro", registro)],
states={NOMBRE: [MessageHandler(filters.TEXT & ~filters.COMMAND, con_nombre)],
CIUDAD: [MessageHandler(filters.TEXT & ~filters.COMMAND, con_ciudad)]},
fallbacks=[CommandHandler("cancel", cancel)],
conversation_timeout=600,
)
app.add_handler(conv) # agrégalo antes del handler de eco
Dibuja primero los estados en el diseñador de flujos; es mucho más fácil que depurar una conversación enredada.
5. Menú de comandos y botones inline
Define el menú de comandos al arrancar con un hook post_init:
from telegram import BotCommand
async def post_init(app):
await app.bot.set_my_commands([
BotCommand("start", "Iniciar el bot"),
BotCommand("registro", "Registrarte"),
BotCommand("cancel", "Cancelar la acción actual"),
])
app = Application.builder().token(os.environ["BOT_TOKEN"]).post_init(post_init).build()
Para botones debajo de un mensaje, usa InlineKeyboardMarkup y un CallbackQueryHandler, y llama siempre a query.answer(). Nuestro artículo sobre teclados inline y callback queries tiene un ejemplo completo, y el constructor de teclados diseña la distribución.
6. Manejo de errores y persistencia
Registra un handler de errores para que las excepciones queden en el log en lugar de desaparecer:
async def on_error(update, context):
logging.exception("Un update causó un error", exc_info=context.error)
app.add_error_handler(on_error)
context.user_data y context.chat_data viven en memoria y se pierden al reiniciar. Para bots pequeños, PicklePersistence los guarda en un archivo:
from telegram.ext import PicklePersistence
persistence = PicklePersistence(filepath="data/bot.pickle")
app = Application.builder().token(TOKEN).persistence(persistence).build()
Marca las conversaciones con persistent=True, name="registro" para conservar también su estado. Para algo más grande, usa una base de datos real; mira bases de datos y estado para bots.
7. Tareas programadas
Con el extra job-queue, context.job_queue programa callbacks:
async def recordar(context):
await context.bot.send_message(context.job.chat_id, f"Recordatorio: {context.job.data}")
async def recordar_cmd(update, context):
minutos = int(context.args[0]); texto = " ".join(context.args[1:]) or "se acabó el tiempo"
context.job_queue.run_once(recordar, when=minutos * 60, chat_id=update.effective_chat.id, data=texto)
await update.message.reply_text(f"Te aviso en {minutos} min.")
Las tareas viven en memoria, así que se pierden al reiniciar. Los recordatorios duraderos necesitan almacenamiento; mira mensajes programados y recordatorios.
8. Despliega: polling o webhook
Polling (lo más simple): corre python bot.py como proceso 24/7 en un VPS o un servicio tipo worker. run_polling maneja SIGTERM para reinicios limpios. Contenerízalo con esta configuración de Docker.
Webhook: necesita una URL HTTPS pública, pero permite que el hosting escale y duerma entre peticiones.
app.run_webhook(
listen="0.0.0.0",
port=int(os.environ.get("PORT", 8080)),
url_path="telegram",
webhook_url="https://tu-app.example.com/telegram",
secret_token=os.environ["WEBHOOK_SECRET"],
)
run_webhook registra el webhook en Telegram y revisa el encabezado secreto por ti. Contexto en setWebhook de Telegram paso a paso; opciones de hosting en mejor hosting para bots de Telegram y desplegar un bot en Python.
Lista antes de publicar
- Token en una variable de entorno, logs de
httpxen WARNING. - Handler de errores registrado; logs visibles en el hosting.
- Persistencia o base de datos para todo lo que el usuario espera que sobreviva un reinicio.
- Una sola instancia corriendo con polling.
- Respeta los límites de envío de Telegram en difusiones; mira los límites de tasa explicados.
Fuentes
- Documentación de python-telegram-bot: docs.python-telegram-bot.org
- Wiki y ejemplos de python-telegram-bot: github.com/python-telegram-bot/python-telegram-bot/wiki
- Referencia de la Bot API de Telegram: core.telegram.org/bots/api
- Telegram, BotFather y funciones de bots: core.telegram.org/bots/features
Comparativas y herramientas relacionadas
Preguntas frecuentes
¿python-telegram-bot o aiogram?
Ambas son librerías async de Python sólidas. python-telegram-bot tiene documentación extensa y ayudantes integrados para conversaciones y tareas; aiogram está construida sobre aiohttp con un diseño de routers y middlewares. Elige la que te resulte más clara.
¿Por qué mi bot deja de responder al desplegar una segunda copia?
Solo un proceso puede llamar a getUpdates por token. Dos instancias con polling chocan y Telegram responde 409 Conflict a una de ellas. Corre una sola instancia o pasa a webhook.