Docs academy · Guide 3 of 8 · 12 min read

Your first bot in discord.py

Build a working bot with a slash command in Python, using discord.py 2.x.


By the end of this guide you'll have a bot online that answers a /ping command. You need the application, token and test server from Start here.

Set up the project

Install a current version of Python, then create a project with a virtual environment, which keeps its packages separate from the rest of your computer.

On macOS or Linux:

mkdir my-bot
cd my-bot
python3 -m venv .venv
source .venv/bin/activate
pip install discord.py python-dotenv

On Windows, the activate line is different:

.venv\Scripts\activate

Create a .env file in the same folder. DISCORD_GUILD_ID is your test server's ID. To get it, turn on Developer Mode in Discord's advanced settings, then right-click your server and click Copy Server ID.

DISCORD_TOKEN=paste-your-token-here
DISCORD_GUILD_ID=your-test-server-id

Add .env and .venv/ to a .gitignore file now, before you forget.

Write the bot

Save this as bot.py:

import os

import discord
from discord import app_commands
from dotenv import load_dotenv

load_dotenv()

TEST_GUILD = discord.Object(id=int(os.environ["DISCORD_GUILD_ID"]))


class MyBot(discord.Client):
    def __init__(self):
        super().__init__(intents=discord.Intents.default())
        self.tree = app_commands.CommandTree(self)

    async def setup_hook(self):
        # Registers the commands with Discord. In your test server this is instant.
        self.tree.copy_global_to(guild=TEST_GUILD)
        await self.tree.sync(guild=TEST_GUILD)


bot = MyBot()


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


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


bot.run(os.environ["DISCORD_TOKEN"])

Start it:

python bot.py

When you see Logged in as, your bot is online. Go to your test server and type /ping.

What just happened

  • Intents tell Discord which events to send you. Intents.default() is everything except the privileged ones. Guide 4 explains those.
  • CommandTree holds your slash commands.
  • setup_hook runs once while the bot starts up, which is the right place to sync commands. Don't sync in on_ready, because that can fire many times.
  • interaction.response.send_message answers the command. Discord expects a reply within 3 seconds.

If your command needs longer than 3 seconds, say so first, then answer when you're done:

await interaction.response.defer()
result = await do_slow_work()
await interaction.followup.send(result)

Keep slow work asynchronous. A blocking call such as time.sleep or requests.get freezes the whole bot, including its connection to Discord. Use asyncio.sleep and an async library such as aiohttp instead.

Options on a command

Function parameters become command options, and type hints tell Discord what to accept:

@bot.tree.command(description="Say hello to someone")
@app_commands.describe(member="Who to greet")
async def hello(interaction: discord.Interaction, member: discord.Member):
    await interaction.response.send_message(f"Hello, {member.mention}!")

Restart your bot and setup_hook syncs the new command for you.

Don't let errors kill your bot

Handle failures in one place and tell the user what happened:

@bot.tree.error
async def on_app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
    print(f"Command failed: {error}")
    message = "Something went wrong running that command."
    if interaction.response.is_done():
        await interaction.followup.send(message, ephemeral=True)
    else:
        await interaction.response.send_message(message, ephemeral=True)

Common problems

| What you see | What it usually means | | --- | --- | | LoginFailure: Improper token | The token is wrong, or .env isn't being read. | | KeyError: 'DISCORD_TOKEN' | .env is missing or in a different folder. | | /ping doesn't appear | The invite link was missing applications.commands, or DISCORD_GUILD_ID points at another server. | | "The application did not respond" | Your bot isn't running, or it took more than 3 seconds without deferring. | | PrivilegedIntentsRequired | You requested a privileged intent that isn't switched on in the Developer Portal. |

Next

Your bot works, but its commands only exist in your test server. Continue with Slash commands, intents and permissions.

Still stuck? Open a ticket and a mentor will take a look. We'll know which guide you were on.