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
Member
User
TextChannel
Role
@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
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
Use meaningful command names
Make commands intuitive and easy to remember.
Docstrings become the command help text.
Check arguments before using them to prevent errors.
Type hints enable automatic conversion and better IDE support.
Handle permissions properly
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