DEV Community

How to Add a Command Menu to a Telegram Bot with Python

A Telegram bot can recognize commands without displaying them in the client. However, users should not have to memorize every available command. Registering a command menu lets Telegram show a list when someone taps the menu button or types /.

In this tutorial, we will create /start, /help, and /status handlers, register them through the Bot API, and display different command menus in private chats and groups.

What Is a Telegram Bot Command Menu?

A bot command begins with a slash:

/start
/help
/status
Enter fullscreen mode Exit fullscreen mode

A registered command has two parts:

  • command: the value users send, without the leading slash.
  • description: a short explanation shown in the Telegram interface.

For example:

start - Start the bot
help - Show available commands
status - Check the service status
Enter fullscreen mode Exit fullscreen mode

Registering this list improves discoverability, but it does not create the command logic. Your Python application must still contain a handler for every command.

If /status appears in the menu but no CommandHandler exists for it, Telegram will send the command to the bot, but the program will not know how to respond.

Prerequisites

You will need:

  • Python 3.10 or newer
  • A Telegram bot created through BotFather
  • A valid bot token
  • The python-telegram-bot package
  • Basic knowledge of Python functions

If the Telegram client is not installed on your test device, follow this Telegram download guide before starting.

Create a project directory:

mkdir telegram-command-menu
cd telegram-command-menu
Enter fullscreen mode Exit fullscreen mode

Create a virtual environment:

python -m venv .venv
Enter fullscreen mode Exit fullscreen mode

Activate it on macOS or Linux:

source .venv/bin/activate
Enter fullscreen mode Exit fullscreen mode

Activate it in Windows PowerShell:

.venv\Scripts\Activate.ps1
Enter fullscreen mode Exit fullscreen mode

Install the library:

python -m pip install --upgrade python-telegram-bot
Enter fullscreen mode Exit fullscreen mode

Store the bot token in an environment variable.

macOS or Linux:

export TELEGRAM_BOT_TOKEN='PASTE_YOUR_TOKEN_HERE'
Enter fullscreen mode Exit fullscreen mode

Windows PowerShell:

$env:TELEGRAM_BOT_TOKEN="PASTE_YOUR_TOKEN_HERE"
Enter fullscreen mode Exit fullscreen mode

Do not place a real token directly in a public code sample or repository.

Create the Command Handlers

Create a file named bot.py:

import os

from telegram import Update
from telegram.ext import (
    Application,
    CommandHandler,
    ContextTypes,
)


async def start(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    if update.message is None:
        return

    user = update.effective_user
    name = user.first_name if user else "there"

    await update.message.reply_text(
        f"Hello, {name}! Use /help to view available commands."
    )


async def help_command(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    if update.message is None:
        return

    await update.message.reply_text(
        "Available commands:\n\n"
        "/start - Start the bot\n"
        "/help - Show this help message\n"
        "/status - Check the service status"
    )


async def status(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    if update.message is None:
        return

    await update.message.reply_text(
        "🟢 The service is operating normally."
    )


def main() -> None:
    token = os.environ.get("TELEGRAM_BOT_TOKEN")

    if not token:
        raise RuntimeError(
            "The TELEGRAM_BOT_TOKEN environment variable is missing."
        )

    application = Application.builder().token(token).build()

    application.add_handler(CommandHandler("start", start))
    application.add_handler(CommandHandler("help", help_command))
    application.add_handler(CommandHandler("status", status))

    application.run_polling()


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Run the program:

python bot.py
Enter fullscreen mode Exit fullscreen mode

Open the bot and test:

/start
/help
/status
Enter fullscreen mode Exit fullscreen mode

The handlers work, but the Telegram command menu may still be empty. The next step is to register the commands.

Method 1: Register Commands with BotFather

For a small bot with a fixed command list, BotFather provides a simple setup method.

Open @BotFather and send:

/mybots
Enter fullscreen mode Exit fullscreen mode

Then select:

Your Bot
→ Edit Bot
→ Edit Commands
Enter fullscreen mode Exit fullscreen mode

Submit one command per line:

start - Start the bot
help - Show available commands
status - Check the service status
Enter fullscreen mode Exit fullscreen mode

After saving, reopen the bot or type / in the message field. Telegram should display the menu.

This approach is convenient when:

  • The command list rarely changes.
  • The bot has only one language.
  • Every chat uses the same commands.
  • Command registration does not need to be part of deployment.

For a larger project, registering commands from Python keeps the visible menu synchronized with the code.

Method 2: Register Commands from Python

Import BotCommand:

from telegram import BotCommand, Update
Enter fullscreen mode Exit fullscreen mode

Create a startup function:

async def set_commands(application: Application) -> None:
    commands = [
        BotCommand("start", "Start the bot"),
        BotCommand("help", "Show available commands"),
        BotCommand("status", "Check the service status"),
    ]

    await application.bot.set_my_commands(commands)
Enter fullscreen mode Exit fullscreen mode

Update the application builder:

application = (
    Application.builder()
    .token(token)
    .post_init(set_commands)
    .build()
)
Enter fullscreen mode Exit fullscreen mode

The post_init callback runs after the application has been initialized and can access application.bot.

The relevant section now looks like this:

async def set_commands(application: Application) -> None:
    commands = [
        BotCommand("start", "Start the bot"),
        BotCommand("help", "Show available commands"),
        BotCommand("status", "Check the service status"),
    ]

    await application.bot.set_my_commands(commands)


def main() -> None:
    token = os.environ.get("TELEGRAM_BOT_TOKEN")

    if not token:
        raise RuntimeError(
            "The TELEGRAM_BOT_TOKEN environment variable is missing."
        )

    application = (
        Application.builder()
        .token(token)
        .post_init(set_commands)
        .build()
    )

    application.add_handler(CommandHandler("start", start))
    application.add_handler(CommandHandler("help", help_command))
    application.add_handler(CommandHandler("status", status))

    application.run_polling()
Enter fullscreen mode Exit fullscreen mode

Restart the bot:

python bot.py
Enter fullscreen mode Exit fullscreen mode

The command menu is now registered automatically whenever the application starts.

Telegram documents this operation in the official setMyCommands reference.

Command Naming Rules

A command name should be simple and predictable.

Good examples:

start
help
status
account
language
server_1
Enter fullscreen mode Exit fullscreen mode

Avoid:

Start
show-status
my command
/settings!
Enter fullscreen mode Exit fullscreen mode

Bot commands use lowercase English letters, digits, and underscores. Do not include the leading slash when creating a BotCommand.

Correct:

BotCommand("status", "Check service status")
Enter fullscreen mode Exit fullscreen mode

Incorrect:

BotCommand("/status", "Check service status")
Enter fullscreen mode Exit fullscreen mode

Descriptions should explain the result of using the command. Avoid vague descriptions such as “Click here” or “Do something.”

Create Different Menus for Private Chats and Groups

A bot may need one menu in private chats and another in groups.

Private commands might include:

/start
/help
/settings
/status
Enter fullscreen mode Exit fullscreen mode

Group commands might include:

/help
/rules
/report
/status
Enter fullscreen mode Exit fullscreen mode

Import the required scopes:

from telegram import (
    BotCommand,
    BotCommandScopeAllGroupChats,
    BotCommandScopeAllPrivateChats,
    Update,
)
Enter fullscreen mode Exit fullscreen mode

Replace set_commands() with:

async def set_commands(application: Application) -> None:
    private_commands = [
        BotCommand("start", "Start the bot"),
        BotCommand("help", "Show available commands"),
        BotCommand("settings", "Open personal settings"),
        BotCommand("status", "Check service status"),
    ]

    group_commands = [
        BotCommand("help", "Show group commands"),
        BotCommand("rules", "Display the group rules"),
        BotCommand("report", "Report a message to moderators"),
        BotCommand("status", "Check bot status"),
    ]

    await application.bot.set_my_commands(
        private_commands,
        scope=BotCommandScopeAllPrivateChats(),
    )

    await application.bot.set_my_commands(
        group_commands,
        scope=BotCommandScopeAllGroupChats(),
    )
Enter fullscreen mode Exit fullscreen mode

The visible menu now depends on the chat type.

However, the menu and the handlers remain separate. If /rules is displayed for groups, a handler must also be registered:

application.add_handler(
    CommandHandler("rules", rules)
)
Enter fullscreen mode Exit fullscreen mode

Telegram supports several command scopes, including default commands, all private chats, all group chats, administrators, specific chats, and specific chat members. The selection rules are documented in the official BotCommandScope reference.

Add Group-Only Command Handlers

A command visible in a group menu should normally reject private-chat usage.

Import ChatType:

from telegram.constants import ChatType
Enter fullscreen mode Exit fullscreen mode

Create a /rules handler:

async def rules(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    if update.message is None:
        return

    chat = update.effective_chat

    if chat is None:
        return

    if chat.type == ChatType.PRIVATE:
        await update.message.reply_text(
            "The /rules command is available in groups only."
        )
        return

    await update.message.reply_text(
        "Group rules:\n\n"
        "1. Be respectful.\n"
        "2. Stay on topic.\n"
        "3. Do not post sensitive information."
    )
Enter fullscreen mode Exit fullscreen mode

Register it:

application.add_handler(CommandHandler("rules", rules))
Enter fullscreen mode Exit fullscreen mode

Hiding a command from the private-chat menu improves the interface, but it does not enforce authorization. Users may still type a command manually, so the handler must validate the chat type and permissions.

Create Administrator-Only Commands

Some commands should only be visible to group administrators.

Import the administrator scope:

from telegram import BotCommandScopeAllChatAdministrators
Enter fullscreen mode Exit fullscreen mode

Register administrator commands:

admin_commands = [
    BotCommand("report", "Review reported messages"),
    BotCommand("cleanup", "Remove recent unwanted messages"),
]

await application.bot.set_my_commands(
    admin_commands,
    scope=BotCommandScopeAllChatAdministrators(),
)
Enter fullscreen mode Exit fullscreen mode

The menu scope only controls visibility. It does not prove that the caller is authorized.

A sensitive handler must still check the member status:

async def is_group_admin(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> bool:
    chat = update.effective_chat
    user = update.effective_user

    if chat is None or user is None:
        return False

    member = await context.bot.get_chat_member(
        chat_id=chat.id,
        user_id=user.id,
    )

    return member.status in {"administrator", "creator"}
Enter fullscreen mode Exit fullscreen mode

Use the check inside an administrator command:

async def cleanup(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    if update.message is None:
        return

    if not await is_group_admin(update, context):
        await update.message.reply_text(
            "You do not have permission to use this command."
        )
        return

    await update.message.reply_text(
        "Cleanup request accepted."
    )
Enter fullscreen mode Exit fullscreen mode

Never rely on command visibility as an access-control mechanism.

Add Language-Specific Commands

Telegram can register different descriptions for different language codes.

For example:

english_commands = [
    BotCommand("start", "Start the bot"),
    BotCommand("help", "Show available commands"),
]

traditional_chinese_commands = [
    BotCommand("start", "開始使用機器人"),
    BotCommand("help", "查看可用指令"),
]
Enter fullscreen mode Exit fullscreen mode

Register the default menu:

await application.bot.set_my_commands(
    english_commands
)
Enter fullscreen mode Exit fullscreen mode

Register Traditional Chinese descriptions:

await application.bot.set_my_commands(
    traditional_chinese_commands,
    language_code="zh-hant",
)
Enter fullscreen mode Exit fullscreen mode

A default command list should still exist for users whose language does not match a localized menu.

The language code affects menu descriptions, not the command handler itself. /help can inspect update.effective_user.language_code if the bot needs to respond in a different language.

Complete Working Example

import logging
import os

from telegram import (
    BotCommand,
    BotCommandScopeAllGroupChats,
    BotCommandScopeAllPrivateChats,
    Update,
)
from telegram.constants import ChatType
from telegram.ext import (
    Application,
    CommandHandler,
    ContextTypes,
)

logging.basicConfig(
    format=(
        "%(asctime)s - %(name)s - "
        "%(levelname)s - %(message)s"
    ),
    level=logging.INFO,
)

logger = logging.getLogger(__name__)


async def start(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    if update.message is None:
        return

    user = update.effective_user
    name = user.first_name if user else "there"

    await update.message.reply_text(
        f"Hello, {name}! Use /help to view available commands."
    )


async def help_command(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    if update.message is None:
        return

    await update.message.reply_text(
        "Available commands:\n\n"
        "/start - Start the bot\n"
        "/help - Show this message\n"
        "/status - Check service status"
    )


async def status(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    if update.message is None:
        return

    await update.message.reply_text(
        "🟢 The service is operating normally."
    )


async def rules(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    if update.message is None:
        return

    chat = update.effective_chat

    if chat is None or chat.type == ChatType.PRIVATE:
        await update.message.reply_text(
            "The /rules command is available in groups only."
        )
        return

    await update.message.reply_text(
        "Group rules:\n\n"
        "1. Be respectful.\n"
        "2. Stay on topic.\n"
        "3. Do not share sensitive information."
    )


async def set_commands(application: Application) -> None:
    private_commands = [
        BotCommand("start", "Start the bot"),
        BotCommand("help", "Show available commands"),
        BotCommand("status", "Check service status"),
    ]

    group_commands = [
        BotCommand("help", "Show group commands"),
        BotCommand("rules", "Display the group rules"),
        BotCommand("status", "Check bot status"),
    ]

    await application.bot.set_my_commands(
        private_commands,
        scope=BotCommandScopeAllPrivateChats(),
    )

    await application.bot.set_my_commands(
        group_commands,
        scope=BotCommandScopeAllGroupChats(),
    )


async def error_handler(
    update: object,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    logger.error(
        "An exception occurred while processing an update.",
        exc_info=context.error,
    )


def main() -> None:
    token = os.environ.get("TELEGRAM_BOT_TOKEN")

    if not token:
        raise RuntimeError(
            "The TELEGRAM_BOT_TOKEN environment variable is missing."
        )

    application = (
        Application.builder()
        .token(token)
        .post_init(set_commands)
        .build()
    )

    application.add_handler(CommandHandler("start", start))
    application.add_handler(CommandHandler("help", help_command))
    application.add_handler(CommandHandler("status", status))
    application.add_handler(CommandHandler("rules", rules))
    application.add_error_handler(error_handler)

    application.run_polling()


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Run:

python bot.py
Enter fullscreen mode Exit fullscreen mode

Test the bot in both a private chat and a group. The displayed commands should differ according to the selected scope.

Common Problems

Commands Work but the Menu Is Empty

The handlers exist, but the command list has not been registered. Use BotFather or set_my_commands().

After registering commands, close and reopen the chat if the client does not refresh immediately.

A Command Appears but Does Nothing

The command was registered in the menu, but no matching handler exists.

For every visible command, confirm that the application contains:

application.add_handler(
    CommandHandler("command_name", handler_function)
)
Enter fullscreen mode Exit fullscreen mode

Old Commands Still Appear

The application may be registering commands under a different scope. It is also possible that commands were previously set through BotFather or another deployment.

Inspect which scopes are being used, update them consistently, and restart the client if necessary.

Telegram also provides deleteMyCommands for removing a command list from a selected scope.

Group Commands Appear in Private Chats

A default command list may be overriding the intended design, or the private scope was not configured.

Register separate BotCommandScopeAllPrivateChats and BotCommandScopeAllGroupChats lists.

Administrator Commands Are Visible to the Wrong Users

Review the command scope, but also remember that visibility is not authorization. Every sensitive handler must verify the caller's current member status.

Commands Stop Updating After Code Changes

The startup callback may not be running, or the application may be exiting before set_my_commands() succeeds.

Add logging inside the startup function:

async def set_commands(application: Application) -> None:
    logger.info("Registering bot commands")
    # Register commands here.
Enter fullscreen mode Exit fullscreen mode

Do not log the bot token.

Final Design Guidelines

A useful command menu should be:

  • Short enough to scan quickly
  • Consistent with implemented handlers
  • Different where private and group workflows differ
  • Localized when the bot serves multiple languages
  • Protected by server-side permission checks
  • Updated as part of deployment

Avoid registering every internal or debugging command. Public menus should contain only actions that ordinary users need.

The command menu improves discoverability, while handlers provide behavior and authorization. Both parts must be maintained together.

繁體中文摘要

Telegram Bot 的指令選單可以讓使用者在輸入 / 時看到 /start/help/status 等功能。指令可以透過 BotFather 手動設定,也可以使用 set_my_commands() 在 Python 程式啟動時自動註冊。

私人聊天、群組及管理員可以使用不同的 BotCommandScope。不過,選單只控制顯示內容,不代表使用者具有操作權限。管理功能仍需在處理函式中驗證聊天類型與管理員身分。


Disclosure: This tutorial was drafted with AI assistance and reviewed against the Telegram Bot API and python-telegram-bot documentation. Test all permission-sensitive commands with a development bot before production use.

Top comments (0)