If you are building a modern Discord bot, learning how to write a discordpy slash komut — a slash command in discord.py — is no longer optional. The old !command style of prefix-based text commands has given way to slash commands, where Discord shows autocomplete and parameter hints as the user types. The app_commands module that ships with discord.py 2.x is the official way to do this, and when set up correctly it gives you a clean, maintainable codebase.
In this guide we start from scratch and work up to parameterized commands, a modular structure with Cogs, and proper error handling. Every example works with discord.py 2.x and Python 3.9+.
Environment and a basic bot skeleton
First install the library at a current version. Older releases do not include app_commands, so 2.x is required:
pip install -U "discord.py>=2.3"
Now a minimal bot. The commands.Bot class automatically exposes a CommandTree — which holds your slash commands — through bot.tree:
import discord
from discord import app_commands
from discord.ext import commands
intents = discord.Intents.default()
bot = commands.Bot(command_prefix="!", intents=intents)
@bot.event
async def on_ready():
print(f"Logged in as {bot.user}")
bot.run("TOKEN")
You do not need the message_content intent for slash commands; that one is only relevant for prefix commands. Never hardcode your token — read it from an environment variable.
Your first slash command and syncing
The simplest slash command is defined with the bot.tree.command decorator. The first parameter is always typed as discord.Interaction:
@bot.tree.command(name="hello", description="Says hello")
async def hello(interaction: discord.Interaction):
await interaction.response.send_message(
f"Hello, {interaction.user.mention}!"
)
The key point: defining the command is not enough — you must sync it to Discord. A global sync (await bot.tree.sync()) applies to every server but can take up to an hour to propagate. That is far too slow during development, so sync to a single test guild instead — this is instant:
GUILD = discord.Object(id=123456789012345678) # test guild ID
@bot.event
async def on_ready():
bot.tree.copy_global_to(guild=GUILD)
await bot.tree.sync(guild=GUILD)
print("Commands synced to guild")
Warning: sync() is a rate-limited API call. Do not blindly call sync() on every startup; run it only when commands actually change. A common pattern is to bind syncing to a hidden prefix command that only the owner can trigger.
Parameters, descriptions and choices
The power of slash commands comes from type-hinted parameters. discord.py reads your Python type hints and generates the right input field: int becomes a number, discord.Member a user picker, bool a toggle.
@bot.tree.command(name="add", description="Adds two numbers")
@app_commands.describe(a="First number", b="Second number")
async def add(interaction: discord.Interaction, a: int, b: int):
await interaction.response.send_message(f"{a} + {b} = {a + b}")
@app_commands.describe sets the help text shown next to each parameter — important for usability. To offer a fixed set of options, use Choice:
@bot.tree.command(name="difficulty", description="Pick a difficulty")
@app_commands.describe(level="Gameplay difficulty")
@app_commands.choices(level=[
app_commands.Choice(name="Easy", value="easy"),
app_commands.Choice(name="Hard", value="hard"),
])
async def difficulty(interaction: discord.Interaction,
level: app_commands.Choice[str]):
await interaction.response.send_message(
f"Selected: {level.name} ({level.value})"
)
If computing the reply will take a while (for example an API call), call await interaction.response.defer() first to avoid exceeding the 3-second response window, then send the result with await interaction.followup.send(...).
Modular structure with Cogs
Managing dozens of commands in one file falls apart quickly. commands.Cog lets you group related commands into a class and split them across separate files (extensions). Inside a Cog you use the @app_commands.command decorator, and the first parameter becomes self:
# cogs/tools.py
import discord
from discord import app_commands
from discord.ext import commands
class Tools(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
@app_commands.command(name="ping", description="Shows latency")
async def ping(self, interaction: discord.Interaction):
latency = round(self.bot.latency * 1000)
await interaction.response.send_message(f"Pong! {latency}ms")
async def setup(bot: commands.Bot):
await bot.add_cog(Tools(bot))
The right place to load extensions is setup_hook, which runs before the bot connects. Subclassing the bot is the cleanest approach:
class Bot(commands.Bot):
async def setup_hook(self):
await self.load_extension("cogs.tools")
await self.tree.sync() # sync to a guild during development
intents = discord.Intents.default()
bot = Bot(command_prefix="!", intents=intents)
bot.run("TOKEN")
To gather multiple commands under one name, use app_commands.Group; that yields subcommands such as /settings language and /settings notify.
Error handling
If an exception is raised inside a command, the user just sees a silent failure. It is best to define one central error handler for all slash commands. CommandTree provides an error decorator for exactly this:
@bot.tree.error
async def on_app_command_error(
interaction: discord.Interaction,
error: app_commands.AppCommandError,
):
if isinstance(error, app_commands.MissingPermissions):
message = "You don't have permission for this command."
else:
message = "Something went wrong, please try again."
# Use response if not yet sent, otherwise followup
if interaction.response.is_done():
await interaction.followup.send(message, ephemeral=True)
else:
await interaction.response.send_message(message, ephemeral=True)
ephemeral=True shows the message only to the person who ran the command — ideal for error notices. You can attach a permission check with @app_commands.checks.has_permissions(...) on the command, and catch the failure in the handler above.
Frequently Asked Questions
Why aren't my slash commands showing up in Discord?
It is almost always a sync issue. A global sync() takes time to propagate; during development, sync commands directly to your test guild (guild=...) so they appear instantly. Also make sure the bot was invited with the applications.commands scope.
What's the difference between app_commands and the old commands.command?
commands.command are prefix-based text commands (!command). app_commands are Discord's native slash commands: they offer autocomplete, type validation and visual parameter fields. For new bots, slash commands are preferred.
Should I call sync on every startup?
No. sync() is rate-limited, and needless calls can get you throttled. Only sync when command definitions change; in production it is common to bind this to a manual owner-only command.
Want to take your bot to the next level? From slash commands to moderation systems, music backends and custom dashboard integrations, I can help with your discord.py projects. Share your idea and let's build it together: get in touch.