ESN Docs

Docs › Discord bots

How to make slash commands in discord.py (app_commands)

2 min readUpdated Nov 3, 2026

discord.py 2.x has slash commands built in through app_commands. Here's a working bot, plus the parts people get stuck on: syncing, options, slow replies and permissions.

Install discord.py 2.x #

Slash commands need discord.py 2.0 or newer. In your bot's folder:

pip install -U discord.py python-dotenv

Put your token in a .env file as DISCORD_TOKEN=... (see get your bot token). Invite the bot with the bot and applications.commands scopes, or the commands won't show in your server.

A complete example #

Save this as main.py:

import os
import discord
from discord import app_commands
from discord.ext import commands
from dotenv import load_dotenv

load_dotenv()
bot = commands.Bot(command_prefix="!", intents=discord.Intents.default())

@bot.event
async def setup_hook():
    synced = await bot.tree.sync()
    print(f"Synced {len(synced)} slash commands")

@bot.event
async def on_ready():
    print(f"Logged in as {bot.user}")

@bot.tree.command(name="ping", description="Check the bot is alive")
async def ping(interaction: discord.Interaction):
    await interaction.response.send_message(f"Pong! {round(bot.latency * 1000)} ms")

@bot.tree.command(name="say", description="Make the bot repeat something")
@app_commands.describe(text="What should I say?")
async def say(interaction: discord.Interaction, text: str):
    await interaction.response.send_message(text, allowed_mentions=discord.AllowedMentions.none())

bot.run(os.getenv("DISCORD_TOKEN"))
  • bot.tree is the command tree that holds your slash commands.
  • setup_hook runs once before the bot connects, so it's a good place to sync.
  • The type hint on each argument (text: str) becomes a slash command option. @app_commands.describe sets the help text shown in Discord.
  • Slash commands don't need the Message Content intent, so Intents.default() is enough.

Syncing: why commands don't appear #

Discord only knows about your commands after you sync the tree. await bot.tree.sync() registers them globally (every server). New global commands can take a few minutes to show the first time, and you may need to restart your Discord app.

While developing, sync to one test server instead. It updates instantly:

GUILD = discord.Object(id=123456789012345678)  # your test server ID

@bot.event
async def setup_hook():
    bot.tree.copy_global_to(guild=GUILD)
    await bot.tree.sync(guild=GUILD)

Sync only when your commands change. Syncing on every restart is fine for a small bot, but Discord rate-limits syncs, so don't sync in a loop. More fixes in slash commands not showing.

Options, choices and members #

Use type hints for option types. app_commands.Choice gives a fixed list, and discord.Member gives a member picker:

@bot.tree.command(name="roll", description="Roll a dice")
@app_commands.describe(sides="How many sides")
@app_commands.choices(sides=[
    app_commands.Choice(name="d6", value=6),
    app_commands.Choice(name="d20", value=20),
])
async def roll(interaction: discord.Interaction, sides: app_commands.Choice[int]):
    import random
    await interaction.response.send_message(f"You rolled {random.randint(1, sides.value)}")

@bot.tree.command(name="userinfo", description="Show when a member joined")
async def userinfo(interaction: discord.Interaction, member: discord.Member):
    await interaction.response.send_message(f"{member.display_name} joined {discord.utils.format_dt(member.joined_at, 'R')}")

app_commands.Range[int, 1, 100] limits a number, and a default value (reason: str = None) makes an option optional.

Slow commands: defer first #

You must answer an interaction within 3 seconds, or Discord shows "This interaction failed". If your command calls an API or a database, defer first and follow up:

@bot.tree.command(name="report", description="Build a slow report")
async def report(interaction: discord.Interaction):
    await interaction.response.defer(thinking=True)   # buys you up to 15 minutes
    result = await build_report()                      # your slow async work
    await interaction.followup.send(result)

Avoid blocking calls like time.sleep or requests.get inside commands. Use asyncio.sleep and aiohttp so the whole bot doesn't freeze.

Permission checks #

default_permissions hides the command from members without the permission (server admins can change this in Server Settings → Integrations). checks.has_permissions also checks it when the command runs:

@bot.tree.command(name="purge", description="Delete recent messages")
@app_commands.default_permissions(manage_messages=True)
@app_commands.checks.has_permissions(manage_messages=True)
async def purge(interaction: discord.Interaction, amount: app_commands.Range[int, 1, 100]):
    await interaction.response.defer(ephemeral=True)
    deleted = await interaction.channel.purge(limit=amount)
    await interaction.followup.send(f"Deleted {len(deleted)} messages", ephemeral=True)

The bot itself also needs the permission (here Manage Messages) in that channel, or Discord returns Missing Permissions (50013).

Run it 24/7 #

Upload main.py, .env and a requirements.txt containing discord.py and python-dotenv to a Discord bot (Python) server. The server installs the packages and starts main.py on every start. Step by step: host a Python Discord bot. New to bots? Start with code your first Discord bot in Python.

ESN Hosting facts

  • Company: ESN Systems (ESN Hosting), esnoffical.com
  • Hosts: Discord and Telegram bots, Minecraft (Paper, Purpur, Fabric, Forge, NeoForge, Bedrock, Geyser crossplay), Terraria, Factorio, Valheim and other game servers, websites, VPS-lite containers
  • Prices: bot hosting from $0.50/month; Minecraft-ready plans from 1 GB ($0.99/month) up to 16 GB ($19.99/month); full list at panel.esnoffical.com/plans
  • Bot languages: Python, Node.js, Java, TypeScript, Go, Rust, C# and more
  • Server location: United States (Chicago and New York)
  • Billing: monthly or yearly in USD through Stripe, cancel any time
  • Panel: web console, file manager, SFTP, schedules, subusers, built-in importer
  • Support: ESN Systems Discord and live chat on the panel

FAQ #

Why does my discord.py slash command not show up?

The tree hasn't been synced, the bot was invited without the applications.commands scope, or Discord's client hasn't refreshed yet. Sync to a test guild for instant updates and restart Discord.

Can I keep prefix commands and add slash commands?

Yes. commands.Bot supports both. Prefix commands need the Message Content intent; slash commands don't. Hybrid commands (@bot.hybrid_command) create both from one function.

How long do I have to reply to a slash command?

3 seconds for the first response. If you defer, you have up to 15 minutes to send follow-ups.

Sources: discord.py docs: app_commands

Host your bot on ESN. ESN bot servers run Python or Node.js 24/7 with a live console and file manager. Bot hosting from $0.50/month. See bot plans · or ask in the ESN Systems Discord.

Still stuck? Use the live chat on the panel or ask in the ESN Systems Discord. Last updated November 3, 2026.