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
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
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-botpackage - 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
Create a virtual environment:
python -m venv .venv
Activate it on macOS or Linux:
source .venv/bin/activate
Activate it in Windows PowerShell:
.venv\Scripts\Activate.ps1
Install the library:
python -m pip install --upgrade python-telegram-bot
Store the bot token in an environment variable.
macOS or Linux:
export TELEGRAM_BOT_TOKEN='PASTE_YOUR_TOKEN_HERE'
Windows PowerShell:
$env:TELEGRAM_BOT_TOKEN="PASTE_YOUR_TOKEN_HERE"
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()
Run the program:
python bot.py
Open the bot and test:
/start
/help
/status
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
Then select:
Your Bot
→ Edit Bot
→ Edit Commands
Submit one command per line:
start - Start the bot
help - Show available commands
status - Check the service status
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
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)
Update the application builder:
application = (
Application.builder()
.token(token)
.post_init(set_commands)
.build()
)
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()
Restart the bot:
python bot.py
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
Avoid:
Start
show-status
my command
/settings!
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")
Incorrect:
BotCommand("/status", "Check service status")
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
Group commands might include:
/help
/rules
/report
/status
Import the required scopes:
from telegram import (
BotCommand,
BotCommandScopeAllGroupChats,
BotCommandScopeAllPrivateChats,
Update,
)
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(),
)
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)
)
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
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."
)
Register it:
application.add_handler(CommandHandler("rules", rules))
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
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(),
)
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"}
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."
)
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", "查看可用指令"),
]
Register the default menu:
await application.bot.set_my_commands(
english_commands
)
Register Traditional Chinese descriptions:
await application.bot.set_my_commands(
traditional_chinese_commands,
language_code="zh-hant",
)
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()
Run:
python bot.py
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)
)
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.
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)