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:
Static String
Multiple Prefixes
Dynamic Callable
Mention Prefix
bot = commands.Bot(command_prefix="!")
# Usage: !ping
bot = commands.Bot(command_prefix=["!", "?", "."])
# Usage: !ping, ?ping, or .ping
async def get_prefix(bot, message):
# Custom logic per guild
return "!" if message.guild else "?"
bot = commands.Bot(command_prefix=get_prefix)
bot = commands.Bot(
command_prefix=commands.when_mentioned_or("!")
)
# Usage: @BotName ping or !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
Enable Message Content Intent
Prefix commands require the message_content privileged intent to be enabled in the Discord Developer Portal and in your code.
Use Type Hints
Add type hints to command parameters for automatic conversion and better code clarity.
Implement Error Handlers
Always implement error handlers to provide helpful feedback to users.
Use Cooldowns
Prevent spam and abuse by adding cooldowns to commands.
Organize with Cogs
Split your bot into cogs for better organization and maintainability.