Skip to main content

Discord Slash Commands in Python: Modern Guide

Slash commands are Discord's modern command interface: /command option1:value option2:value with built-in autocomplete, validation, and type-safe menus visible in the Discord client. Unlike prefix commands, slash commands are self-documenting (users see available commands and their arguments in the command picker) and mobile-friendly. In discord.py 2.0 and later, slash commands are defined using @app_commands.command() with type hints and validators, improving clarity and reducing errors.

Your First Slash Command

Slash commands require the @app_commands.command() decorator from discord.py's app_commands extension. Here's a minimal example:

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

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

@bot.event
async def on_ready():
"""Sync app commands when the bot is ready."""
try:
synced = await bot.tree.sync()
print(f'Synced {len(synced)} command(s)')
except Exception as e:
print(f'Failed to sync commands: {e}')

@app_commands.command(name='greet', description='Greet a person')
async def greet(interaction: discord.Interaction, name: str):
"""Respond with a greeting."""
await interaction.response.send_message(f'Hello, {name}!')

bot.run(os.getenv('DISCORD_TOKEN'))

The key differences from prefix commands: the decorator is @app_commands.command() instead of @bot.command(), the parameter is interaction: discord.Interaction instead of ctx, and you call await interaction.response.send_message() instead of await ctx.send(). The parameter name: str is automatically converted to an option in Discord's UI. Call await bot.tree.sync() in on_ready to register slash commands with Discord; this happens once per bot restart.

Command Options with Types and Choices

Slash command options are function parameters with type hints. Discord validates input before sending it to your bot:

from enum import Enum

class Difficulty(str, Enum):
easy = "easy"
medium = "medium"
hard = "hard"

@app_commands.command(name='quiz', description='Start a quiz game')
async def quiz(
interaction: discord.Interaction,
topic: str,
difficulty: Difficulty = Difficulty.medium,
num_questions: int = 5
):
"""Start a quiz with a topic and difficulty level."""
await interaction.response.send_message(
f'Starting {difficulty} {topic} quiz with {num_questions} questions.'
)

# Alternative: choices without enum
@app_commands.command(name='choose')
@app_commands.describe(color='Pick your favorite color')
@app_commands.choices(color=[
app_commands.Choice(name='Red', value='red'),
app_commands.Choice(name='Blue', value='blue'),
app_commands.Choice(name='Green', value='green'),
])
async def choose(interaction: discord.Interaction, color: str):
"""Choose a color from a dropdown menu."""
await interaction.response.send_message(f'You chose: {color}')

Type hints like int, str, bool, discord.Member, and discord.Role are converted to appropriate Discord option types. Enums create dropdown menus in the UI. The @app_commands.choices() decorator defines a fixed list of options; users see a dropdown instead of typing freely. Default values (e.g., Difficulty.medium) make options optional.

Autocomplete: Dynamic Suggestions

Autocomplete fetches suggestions in real-time as the user types. This is powerful for options with many possible values:

async def available_games(
interaction: discord.Interaction,
current: str,
) -> list[app_commands.Choice[str]]:
"""Autocomplete callback for game names."""
games = ['Minecraft', 'Valorant', 'League of Legends', 'Counter-Strike 2']
# Filter games that match the current input
return [
app_commands.Choice(name=game, value=game)
for game in games if game.lower().startswith(current.lower())
]

@app_commands.command(name='startgame')
@app_commands.autocomplete('game_name', available_games)
async def startgame(interaction: discord.Interaction, game_name: str):
"""Start a game session."""
await interaction.response.send_message(f'Starting {game_name}...')

The autocomplete callback receives the current user input and returns a list of app_commands.Choice objects. As the user types, Discord calls this function and shows matching suggestions. This is far better UX than static choices for large option lists.

Command Groups and Subcommands

Organize related slash commands into groups:

# Create a command group
guild_group = app_commands.Group(
name='guild',
description='Guild management commands'
)

@guild_group.command(name='info', description='Show guild information')
async def guild_info(interaction: discord.Interaction):
"""Display information about the guild."""
guild = interaction.guild
embed = discord.Embed(
title=guild.name,
description=f'Members: {guild.member_count}',
color=discord.Color.blue()
)
await interaction.response.send_message(embed=embed)

@guild_group.command(name='settings', description='View guild settings')
async def guild_settings(interaction: discord.Interaction):
"""Show guild configuration."""
await interaction.response.send_message('Guild settings page (placeholder).')

# Register the group with the bot
bot.tree.add_command(guild_group)

Users invoke these as /guild info and /guild settings. Groups make the command menu cleaner and logically organize related functionality.

Permission Checks and Moderation

Use decorators to enforce permissions for slash commands:

@app_commands.command(name='ban')
@app_commands.checks.has_permissions(ban_members=True)
@app_commands.checks.bot_has_permissions(ban_members=True)
@app_commands.describe(
user='User to ban',
reason='Reason for the ban'
)
async def ban_user(
interaction: discord.Interaction,
user: discord.User,
reason: str = 'No reason provided'
):
"""Ban a user from the server."""
try:
await interaction.guild.ban(user, reason=reason)
await interaction.response.send_message(
f'{user} has been banned: {reason}'
)
except discord.Forbidden:
await interaction.response.send_message(
'I cannot ban this user.',
ephemeral=True
)

@ban_user.error
async def ban_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
"""Handle errors for the ban command."""
if isinstance(error, app_commands.MissingPermissions):
await interaction.response.send_message(
'You need the Ban Members permission.',
ephemeral=True
)
elif isinstance(error, app_commands.BotMissingPermissions):
await interaction.response.send_message(
'I don\'t have the Ban Members permission.',
ephemeral=True
)

@app_commands.checks.has_permissions() validates the user's permissions. The ephemeral=True parameter makes the response visible only to the user who invoked the command; others don't see it. This is ideal for error messages or sensitive information.

Deferred Responses and Editing

If your command takes time to process (e.g., querying an API), defer the response to show a loading state:

@app_commands.command(name='slowapi')
async def slowapi(interaction: discord.Interaction):
"""Fetch data from a slow API."""
# Defer the response (shows "thinking" in Discord)
await interaction.response.defer()

# Do slow work
import asyncio
await asyncio.sleep(3) # Simulate slow API call

# Follow up with the result
await interaction.followup.send('API call completed!')

@app_commands.command(name='edit')
async def edit_message(interaction: discord.Interaction):
"""Send a message and then edit it."""
await interaction.response.send_message('Initial message.')

# Later, edit the response
import asyncio
await asyncio.sleep(2)
await interaction.edit_original_response(content='Edited message!')

await interaction.response.defer() acknowledges the interaction without sending a message yet (shows "Bot is thinking..."). Later, call await interaction.followup.send() to send additional messages. For the initial response, await interaction.edit_original_response() modifies it after sending. This prevents Discord's 3-second timeout for responding.

Comparison: Prefix vs. Slash Commands

AspectPrefix CommandSlash Command
Invocation!help command_name/help command_name (built-in menu)
Type validationManual parsingAutomatic, enforced by Discord
AutocompleteMust implement customNative support
Mobile UXPoor (typing required)Excellent (tappable menu)
Default option/command shows helpUsers see all options in menu
Ephemeral responsesNot directly supportedNative ephemeral=True
Learning curveLower for beginnersModern standard, slightly steeper

Key Takeaways

  • Use @app_commands.command() and interaction: discord.Interaction to define slash commands in discord.py 2.0+.
  • Type hints on function parameters become command options; Discord enforces types before reaching your bot.
  • Call await bot.tree.sync() in on_ready to register commands; syncing must happen at least once.
  • Use @app_commands.choices() for fixed option lists or @app_commands.autocomplete() for dynamic suggestions.
  • Organize related commands with app_commands.Group() and subcommands.
  • Use ephemeral=True for private responses and defer() + followup.send() for long-running operations.
  • Apply @app_commands.checks.has_permissions() to enforce permission checks.

Frequently Asked Questions

How often do I need to sync commands?

Sync once when the bot starts (on_ready) or whenever you add/remove commands. Discord caches synced commands, so subsequent restarts don't require re-syncing unless commands change.

Can I have both prefix and slash commands?

Yes, use both @bot.command() and @app_commands.command() decorators on separate functions. Users invoke them differently, but both can coexist.

What is the difference between deferand ephemeral?

Deferral (defer()) delays sending an initial response (useful for slow operations). Ephemeral (ephemeral=True) hides the response from others; it's visible only to the invoking user. They solve different problems: deferis for latency, ephemeral is for privacy.

Can slash command options be lists or arrays?

Discord's native option types don't support arrays. Use multiple separate options or a single string that the user provides in comma-separated format; parse it in code.

How do I show different subcommand options based on user input?

Discord doesn't support conditional option visibility. Design command structure ahead of time. Use groups and separate subcommands if you need different options for different scenarios.

Further Reading