Skip to main content
The ext.commands extension provides a powerful framework for creating text-based (prefix) commands in your Discord bot.

Installation

from discord.ext import commands

Bot Class

The commands.Bot class extends discord.Bot and adds command processing functionality.

Creating a Bot

import discord
from discord.ext import commands

intents = discord.Intents.default()
intents.message_content = True  # Required for prefix commands

bot = commands.Bot(
    command_prefix="!",
    intents=intents,
    help_command=commands.DefaultHelpCommand()
)
Prefixed commands require the message_content privileged intent to be enabled in your bot’s settings and in your code.

Command Prefix Options

The command prefix can be:
bot = commands.Bot(command_prefix="!")
# Usage: !ping

Creating Commands

Basic Command Decorator

@bot.command()
async def ping(ctx):
    """Responds with Pong!"""
    await ctx.send("Pong!")

Command with Arguments

@bot.command()
async def greet(ctx, member: discord.Member, *, message: str = "Hello!"):
    """Greets a member with a custom message."""
    await ctx.send(f"{member.mention}, {message}")

# Usage: !greet @User Welcome to the server!

Command Aliases

@bot.command(aliases=["say", "repeat"])
async def echo(ctx, *, text: str):
    """Repeats your message."""
    await ctx.send(text)

# Usage: !echo text, !say text, or !repeat text

Command Groups

Command groups allow you to create subcommands.
@bot.group(invoke_without_command=True)
async def config(ctx):
    """Configuration commands."""
    await ctx.send("Use a subcommand: config prefix, config role")

@config.command()
async def prefix(ctx, new_prefix: str):
    """Changes the bot prefix."""
    # Save prefix logic here
    await ctx.send(f"Prefix changed to: {new_prefix}")

@config.command()
async def role(ctx, role: discord.Role):
    """Sets the admin role."""
    await ctx.send(f"Admin role set to: {role.name}")

# Usage: !config prefix ?, !config role @Admin

Context Object

The Context object provides information about command invocation:
@bot.command()
async def info(ctx):
    """Shows context information."""
    await ctx.send(
        f"Author: {ctx.author.mention}\n"
        f"Channel: {ctx.channel.name}\n"
        f"Guild: {ctx.guild.name}\n"
        f"Prefix: {ctx.prefix}\n"
        f"Command: {ctx.command.name}"
    )

Context Methods

  • ctx.send() - Send a message to the channel
  • ctx.reply() - Reply to the invoking message
  • ctx.author - The user who invoked the command
  • ctx.guild - The guild where the command was invoked
  • ctx.channel - The channel where the command was invoked
  • ctx.message - The message that triggered the command

Converters

Converters automatically transform string arguments into Discord objects or Python types.

Built-in Converters

@bot.command()
async def userinfo(ctx, user: discord.User):
    """Shows information about a user."""
    await ctx.send(f"ID: {user.id}\nCreated: {user.created_at}")

@bot.command()
async def slowmode(ctx, channel: discord.TextChannel, seconds: int):
    """Sets slowmode for a channel."""
    await channel.edit(slowmode_delay=seconds)
    await ctx.send(f"Slowmode set to {seconds}s in {channel.mention}")
Built-in converters include: discord.Member, discord.User, discord.TextChannel, discord.Role, discord.Emoji, discord.Message, discord.Guild, discord.Colour, and more.

Custom Converters

from discord.ext import commands

class TimeConverter(commands.Converter):
    async def convert(self, ctx, argument):
        units = {"s": 1, "m": 60, "h": 3600, "d": 86400}
        unit = argument[-1]
        if unit not in units:
            raise commands.BadArgument("Invalid time unit")
        
        try:
            value = int(argument[:-1])
        except ValueError:
            raise commands.BadArgument("Invalid time value")
        
        return value * units[unit]

@bot.command()
async def remind(ctx, duration: TimeConverter, *, reminder: str):
    """Sets a reminder."""
    await ctx.send(f"I'll remind you in {duration} seconds: {reminder}")
    await asyncio.sleep(duration)
    await ctx.send(f"{ctx.author.mention}, reminder: {reminder}")

# Usage: !remind 5m Buy milk

Greedy Converter

from discord.ext.commands import Greedy

@bot.command()
async def ban(ctx, members: Greedy[discord.Member], *, reason: str = None):
    """Bans multiple members."""
    for member in members:
        await member.ban(reason=reason)
    await ctx.send(f"Banned {len(members)} member(s)")

# Usage: !ban @User1 @User2 @User3 Spamming

Checks

Checks are predicates that must pass before a command executes.

Built-in Checks

from discord.ext.commands import has_permissions, guild_only, is_owner

@bot.command()
@has_permissions(manage_messages=True)
async def clear(ctx, amount: int):
    """Clears messages (requires Manage Messages)."""
    await ctx.channel.purge(limit=amount + 1)

@bot.command()
@guild_only()
async def serverinfo(ctx):
    """Shows server information (DM only)."""
    await ctx.send(f"Server: {ctx.guild.name}\nMembers: {ctx.guild.member_count}")

@bot.command()
@is_owner()
async def restart(ctx):
    """Restarts the bot (owner only)."""
    await ctx.send("Restarting...")
    await bot.close()

Custom Checks

def is_moderator():
    async def predicate(ctx):
        mod_role = discord.utils.get(ctx.guild.roles, name="Moderator")
        return mod_role in ctx.author.roles
    return commands.check(predicate)

@bot.command()
@is_moderator()
async def announce(ctx, *, message: str):
    """Makes an announcement (moderators only)."""
    await ctx.send(f"📢 **Announcement:** {message}")

Cooldowns

Cooldowns prevent command spam.
from discord.ext.commands import cooldown, BucketType

@bot.command()
@cooldown(1, 60, BucketType.user)  # 1 use per 60 seconds per user
async def daily(ctx):
    """Claims daily reward."""
    await ctx.send("You claimed 100 coins!")

@daily.error
async def daily_error(ctx, error):
    if isinstance(error, commands.CommandOnCooldown):
        await ctx.send(f"Try again in {error.retry_after:.0f} seconds")

Bucket Types

  • BucketType.default - Global cooldown
  • BucketType.user - Per user
  • BucketType.guild - Per guild
  • BucketType.channel - Per channel
  • BucketType.member - Per guild member
  • BucketType.category - Per channel category
  • BucketType.role - Per role

Error Handling

Command Error Handler

@bot.event
async def on_command_error(ctx, error):
    if isinstance(error, commands.MissingRequiredArgument):
        await ctx.send(f"Missing argument: {error.param.name}")
    elif isinstance(error, commands.BadArgument):
        await ctx.send("Invalid argument provided.")
    elif isinstance(error, commands.MissingPermissions):
        await ctx.send("You don't have permission to use this command.")
    elif isinstance(error, commands.CommandNotFound):
        return  # Ignore unknown commands
    else:
        raise error  # Re-raise unknown errors

Local Error Handler

@bot.command()
async def divide(ctx, a: int, b: int):
    """Divides two numbers."""
    result = a / b
    await ctx.send(f"{a} / {b} = {result}")

@divide.error
async def divide_error(ctx, error):
    if isinstance(error, ZeroDivisionError):
        await ctx.send("Cannot divide by zero!")
    elif isinstance(error, commands.BadArgument):
        await ctx.send("Please provide valid numbers.")

Cogs

Cogs allow you to organize commands into separate files.

Creating a Cog

from discord.ext import commands

class Moderation(commands.Cog):
    """Moderation commands."""
    
    def __init__(self, bot):
        self.bot = bot
    
    @commands.command()
    @commands.has_permissions(kick_members=True)
    async def kick(self, ctx, member: discord.Member, *, reason: str = None):
        """Kicks a member from the server."""
        await member.kick(reason=reason)
        await ctx.send(f"{member} was kicked.")
    
    @commands.command()
    @commands.has_permissions(ban_members=True)
    async def ban(self, ctx, member: discord.Member, *, reason: str = None):
        """Bans a member from the server."""
        await member.ban(reason=reason)
        await ctx.send(f"{member} was banned.")
    
    @commands.Cog.listener()
    async def on_member_join(self, member):
        """Welcomes new members."""
        channel = member.guild.system_channel
        if channel:
            await channel.send(f"Welcome {member.mention}!")

async def setup(bot):
    await bot.add_cog(Moderation(bot))

Loading Cogs

# In your main bot file
import asyncio

async def main():
    async with bot:
        await bot.load_extension("cogs.moderation")
        await bot.start("YOUR_TOKEN")

asyncio.run(main())

Hooks

Hooks run before or after command execution.
@bot.before_invoke
async def before_any_command(ctx):
    """Runs before every command."""
    print(f"{ctx.author} used {ctx.command.name}")

@bot.after_invoke
async def after_any_command(ctx):
    """Runs after every command."""
    print(f"{ctx.command.name} completed")

@bot.command()
async def test(ctx):
    await ctx.send("Test command!")

@test.before_invoke
async def before_test(ctx):
    """Runs before test command."""
    await ctx.send("Preparing test...")

Help Command

Customize the built-in help command:
from discord.ext.commands import DefaultHelpCommand

class MyHelpCommand(DefaultHelpCommand):
    def __init__(self):
        super().__init__(
            no_category="Commands",
            commands_heading="Available Commands:",
            aliases_heading="Aliases:"
        )
    
    async def send_bot_help(self, mapping):
        embed = discord.Embed(
            title="Bot Help",
            description="Here are all available commands:",
            color=discord.Color.blue()
        )
        # Custom help formatting
        await self.get_destination().send(embed=embed)

bot = commands.Bot(
    command_prefix="!",
    intents=intents,
    help_command=MyHelpCommand()
)
To disable the default help command, set help_command=None when creating your bot.

AutoShardedBot

For large bots that need sharding:
bot = commands.AutoShardedBot(
    command_prefix="!",
    intents=intents
)

# Everything else works the same

Best Practices

1

Enable Message Content Intent

Prefix commands require the message_content privileged intent to be enabled in the Discord Developer Portal and in your code.
2

Use Type Hints

Add type hints to command parameters for automatic conversion and better code clarity.
3

Implement Error Handlers

Always implement error handlers to provide helpful feedback to users.
4

Use Cooldowns

Prevent spam and abuse by adding cooldowns to commands.
5

Organize with Cogs

Split your bot into cogs for better organization and maintainability.

Build docs developers (and LLMs) love