1. Create the application
- In the Discord Developer Portal, click New Application.
- On the Bot page, reset and copy the token. Keep it in an environment variable.
- Leave the privileged intents off. This bot will not need them; Discord gateway intents explained covers why.
- Invite the bot with the
botandapplications.commandsscopes and only the permissions it uses. The permissions calculator generates the URL.
Platform concepts are in the Discord bots guide.
2. Install discord.py
The latest release when this guide was written was discord.py 2.7.1; it requires Python 3.8 or newer, though a current Python release is the better choice.
python3 -m venv .venv && . .venv/bin/activate
pip install "discord.py==2.*"
3. Client with a command tree
# bot.py
import os
import discord
from discord import app_commands
TEST_GUILD = discord.Object(id=int(os.environ["TEST_GUILD_ID"]))
class Bot(discord.Client):
def __init__(self):
super().__init__(intents=discord.Intents.default())
self.tree = app_commands.CommandTree(self)
async def setup_hook(self):
# During development: copy commands to one server for instant updates
self.tree.copy_global_to(guild=TEST_GUILD)
await self.tree.sync(guild=TEST_GUILD)
bot = Bot()
@bot.event
async def on_ready():
print(f"Logged in as {bot.user} ({bot.user.id})")
@bot.tree.command(description="Check that the bot is alive")
async def ping(interaction: discord.Interaction):
await interaction.response.send_message(f"Pong ({round(bot.latency * 1000)} ms)", ephemeral=True)
bot.run(os.environ["DISCORD_TOKEN"])
Intents.default() excludes the privileged intents, and slash commands arrive as interactions anyway. setup_hook runs once before the bot connects, which is the right place to sync.
Syncing in production. tree.sync() without a guild publishes global commands. It is rate limited, so do not call it on every start; sync when commands change, for example from an owner-only command or a deploy script.
4. Typed options and choices
discord.py turns type hints into command options, and Discord validates them before your code runs:
from typing import Literal
@bot.tree.command(description="Roll dice")
@app_commands.describe(sides="Number of sides", count="How many dice")
async def roll(interaction: discord.Interaction,
sides: app_commands.Range[int, 2, 100] = 6,
count: app_commands.Range[int, 1, 10] = 1):
import random
rolls = [random.randint(1, sides) for _ in range(count)]
await interaction.response.send_message(f"{rolls} (total {sum(rolls)})")
@bot.tree.command(description="Set your notification preference")
async def notify(interaction: discord.Interaction, mode: Literal["all", "mentions", "none"]):
await interaction.response.send_message(f"Notifications: {mode}", ephemeral=True)
Literal becomes a fixed list of choices; discord.Member, discord.TextChannel and discord.Role hints become user, channel and role pickers. Preview command JSON with our Discord command builder.
5. Slow work: defer
You have 3 seconds to respond. For anything slower, defer, then send a follow-up within 15 minutes:
@bot.tree.command(description="Summarize a web page")
async def summarize(interaction: discord.Interaction, url: str):
await interaction.response.defer(thinking=True)
text = await fetch_and_summarize(url) # your slow function
await interaction.followup.send(text[:2000]) # message content limit is 2000 characters
Long answers need splitting; the message splitter shows how.
6. Buttons with views
class Confirm(discord.ui.View):
def __init__(self, author_id: int):
super().__init__(timeout=60)
self.author_id = author_id
async def interaction_check(self, interaction: discord.Interaction) -> bool:
return interaction.user.id == self.author_id # only the caller may press
@discord.ui.button(label="Confirm", style=discord.ButtonStyle.danger)
async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.edit_message(content="Done.", view=None)
self.stop()
@bot.tree.command(description="Delete your saved data")
async def forget(interaction: discord.Interaction):
await interaction.response.send_message("Are you sure?", view=Confirm(interaction.user.id), ephemeral=True)
Views are in memory; after a restart, old buttons stop working unless you register persistent views with a fixed custom_id and timeout=None.
7. Permissions and errors
Restrict commands declaratively:
@bot.tree.command(description="Delete recent messages")
@app_commands.default_permissions(manage_messages=True)
@app_commands.guild_only()
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)
default_permissions hides the command from members without that permission (server admins can override it), and the bot itself also needs Manage Messages in that channel.
Catch errors in one place:
@bot.tree.error
async def on_app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
msg = "Something went wrong."
if isinstance(error, app_commands.CheckFailure):
msg = "You cannot use this command here."
if interaction.response.is_done():
await interaction.followup.send(msg, ephemeral=True)
else:
await interaction.response.send_message(msg, ephemeral=True)
raise error # keep the traceback in the logs
If something still fails silently, work through Discord bot not responding.
8. Run it 24/7
A Discord bot holds a gateway connection open, so it needs an always-on process; serverless functions that sleep do not work for gateway bots. Options:
- A small VPS with systemd or Docker: Python bot with Docker, VPS with systemd tutorial.
- A PaaS worker service: deploy a bot on Railway.
- Comparison of hosts: Discord bot hosting 24/7.
Handle SIGTERM so restarts do not leave two sessions running; see graceful shutdown for bots.
Sources
- discord.py documentation: discordpy.readthedocs.io
- discord.py, interactions API reference (app_commands): discordpy.readthedocs.io/en/stable/interactions/api.html
- discord.py repository and examples: github.com/Rapptz/discord.py
- Discord Developer Docs, application commands: discord.com/developers/docs/interactions/application-commands
Related comparisons and tools
Frequently asked questions
Do I need the Message Content intent for discord.py slash commands?
No. Slash commands, buttons and modals arrive as interactions with their own data. Message Content is only needed to read ordinary message text.
Why are my commands not showing up?
They were not synced, or they were synced to a different server. Sync to a test guild while developing, sync globally when releasing, and invite the bot with the applications.commands scope.