Skip to main content
Prefix commands are traditional text-based commands that users trigger by typing a prefix followed by the command name (e.g., !help or !ping).

Setting Up Commands Bot

Use commands.Bot to enable prefix commands:
import discord
from discord.ext import commands

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

bot = commands.Bot(
    command_prefix="!",  # or use a callable
    description="My awesome bot",
    intents=intents,
)

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

bot.run("TOKEN")
Prefix commands require the message_content privileged intent to read message content.

Dynamic Command Prefix

Multiple Prefixes

bot = commands.Bot(command_prefix=["!", "?", "."])

Mention as Prefix

bot = commands.Bot(command_prefix=commands.when_mentioned_or("!"))

Callable Prefix

async def get_prefix(bot, message):
    # Different prefixes per guild
    if message.guild:
        return commands.when_mentioned_or("!")(bot, message)
    return commands.when_mentioned_or("?")(bot, message)

bot = commands.Bot(command_prefix=get_prefix)

Basic Commands

Simple Command

@bot.command()
async def ping(ctx: commands.Context):
    """Check if the bot is responsive."""
    await ctx.send(f"Pong! {round(bot.latency * 1000)}ms")

Command with Arguments

@bot.command()
async def say(ctx: commands.Context, *, message: str):
    """Make the bot repeat your message."""
    await ctx.send(message)

Command with Multiple Arguments

@bot.command()
async def add(ctx: commands.Context, left: int, right: int):
    """Adds two numbers together."""
    await ctx.send(str(left + right))

Argument Parsing

Optional Arguments

@bot.command()
async def greet(ctx: commands.Context, name: str = "friend"):
    """Greet someone."""
    await ctx.send(f"Hello {name}!")

Variable Arguments

@bot.command()
async def choose(ctx: commands.Context, *choices: str):
    """Choose between multiple options."""
    import random
    await ctx.send(random.choice(choices))

Keyword-Only Arguments

Use * to consume all remaining text:
@bot.command()
async def repeat(ctx: commands.Context, times: int, *, content: str = "repeating..."):
    """Repeat a message multiple times."""
    for _ in range(times):
        await ctx.send(content)

Converters

Converters automatically transform string arguments into Python objects:

Built-in Converters

@bot.command()
async def info(ctx: commands.Context, member: discord.Member):
    """Get info about a member."""
    await ctx.send(
        f"{member.mention} joined at {discord.utils.format_dt(member.joined_at)}"
    )

Union Types

from typing import Union

@bot.command()
async def mention(ctx: commands.Context, target: Union[discord.Member, discord.Role]):
    """Mention a member or role."""
    await ctx.send(f"Mentioning: {target.mention}")

Greedy Converter

from discord.ext.commands import Greedy

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

Command Groups

Create parent commands with subcommands:
@bot.group()
async def config(ctx: commands.Context):
    """Configuration commands."""
    if ctx.invoked_subcommand is None:
        await ctx.send("Use a subcommand: prefix, welcome")

@config.command()
async def prefix(ctx: commands.Context, new_prefix: str):
    """Change the command prefix."""
    # Save to database
    await ctx.send(f"Prefix changed to {new_prefix}")

@config.command()
async def welcome(ctx: commands.Context, channel: discord.TextChannel):
    """Set the welcome channel."""
    # Save to database
    await ctx.send(f"Welcome channel set to {channel.mention}")
Usage: !config prefix ? or !config welcome #general

Command Checks

Built-in Checks

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

@bot.command()
@has_permissions(administrator=True)
async def setup(ctx: commands.Context):
    """Admin only command."""
    await ctx.send("Setting up...")

@bot.command()
@guild_only()
async def server_info(ctx: commands.Context):
    """Get server information."""
    await ctx.send(f"Server: {ctx.guild.name}")

@bot.command()
@is_owner()
async def shutdown(ctx: commands.Context):
    """Shut down the bot (owner only)."""
    await ctx.send("Shutting down...")
    await bot.close()

Custom Checks

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

@bot.command()
@is_mod()
async def warn(ctx: commands.Context, member: discord.Member, *, reason: str):
    """Warn a member (moderators only)."""
    await ctx.send(f"⚠️ {member.mention} has been warned: {reason}")

Check Any

from discord.ext.commands import check_any

@bot.command()
@check_any(has_permissions(administrator=True), is_owner())
async def critical(ctx: commands.Context):
    """Admin or owner only."""
    await ctx.send("Critical action performed")

Cooldowns

Basic Cooldown

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: commands.Context):
    """Claim your daily reward."""
    await ctx.send("You claimed 100 coins!")

Cooldown Types

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

Dynamic Cooldown

from discord.ext.commands import dynamic_cooldown

def custom_cooldown(ctx: commands.Context):
    if ctx.author.id == OWNER_ID:
        return None  # No cooldown for owner
    return commands.Cooldown(1, 60)  # 1 per minute for others

@bot.command()
@dynamic_cooldown(custom_cooldown, BucketType.user)
async def special(ctx: commands.Context):
    await ctx.send("Special command used!")

Cogs

Organize commands into classes:
class Moderation(commands.Cog):
    """Moderation commands."""

    def __init__(self, bot: commands.Bot):
        self.bot = bot

    @commands.command()
    @commands.has_permissions(kick_members=True)
    async def kick(self, ctx: commands.Context, member: discord.Member, *, reason: str = None):
        """Kick a member from the server."""
        await member.kick(reason=reason)
        await ctx.send(f"Kicked {member.mention}")

    @commands.command()
    @commands.has_permissions(ban_members=True)
    async def ban(self, ctx: commands.Context, member: discord.Member, *, reason: str = None):
        """Ban a member from the server."""
        await member.ban(reason=reason)
        await ctx.send(f"Banned {member.mention}")

    @commands.Cog.listener()
    async def on_member_join(self, member: discord.Member):
        channel = member.guild.system_channel
        if channel:
            await channel.send(f"Welcome {member.mention}!")

# Load the cog
bot.add_cog(Moderation(bot))

Help Command

Custom Help Command

class MyHelp(commands.HelpCommand):
    async def send_bot_help(self, mapping):
        embed = discord.Embed(title="Bot Commands", color=discord.Color.blue())
        for cog, commands in mapping.items():
            if commands:
                cog_name = getattr(cog, "qualified_name", "No Category")
                command_list = ", ".join(c.name for c in commands)
                embed.add_field(name=cog_name, value=command_list, inline=False)
        await self.get_destination().send(embed=embed)

bot.help_command = MyHelp()

Remove Default Help

bot.help_command = None

Before/After Invoke

Run code before or after command execution:
@bot.before_invoke
async def before_command(ctx: commands.Context):
    print(f"{ctx.author} used {ctx.command}")

@bot.after_invoke
async def after_command(ctx: commands.Context):
    print(f"{ctx.command} finished")
Per-command hooks:
@bot.command()
async def database(ctx: commands.Context):
    """Command that uses database."""
    # Command logic
    pass

@database.before_invoke
async def ensure_connection(ctx: commands.Context):
    # Ensure database is connected
    pass

@database.after_invoke
async def close_connection(ctx: commands.Context):
    # Close database connection
    pass

Context Information

Access useful information from the context:
@bot.command()
async def info(ctx: commands.Context):
    embed = discord.Embed(title="Context Info")
    embed.add_field(name="Author", value=ctx.author.mention)
    embed.add_field(name="Channel", value=ctx.channel.mention)
    embed.add_field(name="Guild", value=ctx.guild.name if ctx.guild else "DM")
    embed.add_field(name="Message", value=ctx.message.jump_url)
    await ctx.send(embed=embed)

Best Practices

1
Use meaningful command names
2
Make commands intuitive and easy to remember.
3
Add docstrings
4
Docstrings become the command help text.
5
Validate input
6
Check arguments before using them to prevent errors.
7
Use type hints
8
Type hints enable automatic conversion and better IDE support.
9
Handle permissions properly
10
Use checks to restrict command access appropriately.
Common Mistakes:
  • Forgetting to enable the message_content intent
  • Not handling missing permissions in checks
  • Using await on non-coroutine functions
  • Forgetting to pass ctx as the first parameter in cog commands

See Also

Build docs developers (and LLMs) love