Guía 28

Crea un bot de Telegram en Python con python-telegram-bot, paso a paso

python-telegram-bot es un envoltorio maduro y totalmente asíncrono de la Bot API de Telegram. Esta guía construye un bot pequeño pero completo que puedes ampliar.

Guía4 min de lecturaActualizado

1. Crea el bot en BotFather

  1. Abre @BotFather en Telegram y envía /newbot.
  2. Elige un nombre visible y un nombre de usuario que termine en bot.
  3. 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.COMMAND coincide con texto que no es comando, así que /start no se repite.
  • Silenciar el logger de httpx importa: 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 httpx en 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

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.