A Discord bot can automate tasks, engage communities, and add custom features to your server. Python, with its simple syntax and powerful libraries, is an excellent choice to build one.
In this article, you'll learn essential techniques to get started, from initial setup to deployment. You'll get practical tips, explore real-world applications, and receive debugging advice to ensure your bot runs smoothly.
Basic setup with discord.py
import discord
from discord.ext import commands
bot = commands.Bot(command_prefix='!', intents=discord.Intents.default())
@bot.event
async def on_ready():
print(f'{bot.user} has connected to Discord!')
bot.run('YOUR_TOKEN_HERE')MyBot#1234 has connected to Discord!
This setup initializes your bot using the commands.Bot class. Two parameters are key to its function:
command_prefix='!': This defines the character your bot looks for to identify a command. Any message starting with'!'will be processed.intents=discord.Intents.default(): This is essential. Intents declare which events your bot needs from Discord's servers. The default set is a safe starting point, granting access to common events like messages without requiring special permissions.
The @bot.event decorator registers the asynchronous on_ready function to confirm a successful connection, and bot.run() starts the bot using your unique token.
Core bot functionality
With the basic connection established, you can now teach your bot to interact with users, handle commands, and present information in visually appealing ways.
Responding to user messages
@bot.event
async def on_message(message):
if message.author == bot.user:
return
if 'hello' in message.content.lower():
await message.channel.send('Hello there!')
await bot.process_commands(message)User: hello Bot: Hello there!
The on_message event triggers for every message your bot can see. The first check—if message.author == bot.user:—is a critical safeguard. It prevents the bot from replying to its own messages and getting stuck in an infinite loop.
After this check, the bot looks for 'hello' in the message content to send a reply. The final line, await bot.process_commands(message), is vital. It ensures your bot still processes regular commands, so this custom response logic doesn't interfere with your command prefix functionality.
Creating custom commands with @bot.command()
@bot.command(name='ping')
async def ping_command(ctx):
await ctx.send(f'Pong! Latency: {round(bot.latency * 1000)}ms')
@bot.command()
async def echo(ctx, *, message):
await ctx.send(message)User: !ping Bot: Pong! Latency: 42ms User: !echo Hello world Bot: Hello world
The @bot.command() decorator is the standard way to create commands. The function name becomes the command's trigger, though you can override it, as seen with name='ping'. Every command function receives a ctx (context) object, which holds details like the channel and author and lets you send replies with ctx.send().
- The
pingcommand is a simple diagnostic tool that replies with the bot's current latency to Discord's servers. - In the
echocommand, the asterisk in*, messageis a keyword-only argument that captures all text after the command as a single string.
Working with rich embeds for better visuals
@bot.command()
async def info(ctx):
embed = discord.Embed(title="Bot Info", description="A cool Discord bot", color=0x00ff00)
embed.add_field(name="Author", value="Your Name", inline=False)
embed.add_field(name="Version", value="1.0", inline=True)
await ctx.send(embed=embed)[An embedded message appears with the title "Bot Info", description "A cool Discord bot", and fields for Author and Version]
Embeds are a fantastic way to present information cleanly. Instead of a wall of text, you can create structured, colorful messages. You start by creating a discord.Embed object, where you can set the main title, description, and color.
- Use
embed.add_field()to add organized sections of data. - The
inlineparameter controls layout. Settinginline=Trueallows fields to sit side-by-side, whileinline=Falseensures a field takes up the full width.
Finally, you send the completed object with ctx.send(embed=embed).
Advanced bot development
With the core functionality in place, you can now focus on making your bot more robust and scalable by handling events, organizing code, and managing errors.
Handling different Discord events
@bot.event
async def on_member_join(member):
channel = member.guild.system_channel
if channel:
await channel.send(f'Welcome {member.mention} to the server!')
@bot.event
async def on_reaction_add(reaction, user):
if user != bot.user and str(reaction.emoji) == '👍':
await reaction.message.channel.send(f'{user.name} gave a thumbs up!')[When a new member joins] Bot: Welcome @NewUser to the server! [When someone adds a 👍 reaction] Bot: JohnDoe gave a thumbs up!
Your bot can do more than just respond to commands. The @bot.event decorator lets you listen for specific server activities, making your bot feel more integrated and alive. This allows you to create automated responses to user actions that aren't prefixed commands, though proper memory management becomes crucial for bots handling many events.
- The
on_member_joinevent triggers when a new user joins. The code uses thememberobject to get their details and sends a welcome message to the server's designated system channel. on_reaction_addfires whenever a reaction is added to a message. The example checks if the emoji is'👍'and ensures the bot isn't responding to its own actions before sending a confirmation.
Organizing code with Cogs and extensions
class Greetings(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.command()
async def hello(self, ctx):
await ctx.send(f'Hello, {ctx.author.name}!')
async def setup(bot):
await bot.add_cog(Greetings(bot))[In main file after adding]
await bot.load_extension('greetings')
User: !hello
Bot: Hello, JohnDoe!As your bot gets more complex, Cogs help you keep your code organized. A Cog is a Python class that groups related commands and event listeners together. By placing each Cog in a separate file, known as an extension, you can keep your main bot file clean and manageable.
- A class becomes a Cog by inheriting from
commands.Cog. All its commands and listeners are bundled within it. - Each extension file must contain a
setupfunction. This is what discord.py calls to register the class with your bot usingbot.add_cog(). - You activate everything in the file from your main script by calling
bot.load_extension().
Implementing error handling for robust bots
import logging
logging.basicConfig(level=logging.INFO)
@bot.event
async def on_command_error(ctx, error):
if isinstance(error, commands.MissingRequiredArgument):
await ctx.send('Please provide all required arguments.')
elif isinstance(error, commands.CommandNotFound):
await ctx.send('Command not found.')
else:
logging.error(f'Error: {str(error)}')User: !echo Bot: Please provide all required arguments. User: !unknown Bot: Command not found. [In console/log file] ERROR:root:Error: Something went wrong
A robust bot gracefully handles mistakes. The on_command_error event acts as a global safety net, catching any errors that occur when a command is run. This prevents your bot from crashing and lets you give users helpful feedback, similar to how plan mode provides a safer development approach. Understanding handling multiple exceptions in Python is crucial for building robust error handling systems.
- The code uses
isinstance()to check the specific type of error. It sends a friendly message for common issues likecommands.MissingRequiredArgumentorcommands.CommandNotFound. - For any other unexpected errors, the
elseblock logs the full error details usinglogging.error(). This keeps your server chat clean while giving you the information you need to debug.
Move faster with Replit
Replit is an AI-powered development platform that comes with all Python dependencies pre-installed, so you can skip setup and start coding instantly. You don't need to worry about managing environments or installations.
Instead of piecing together techniques, you can build a complete application. This is where Agent 4 comes in. Describe the bot you want to build, and the Agent handles everything from writing the code to connecting APIs and deploying it.
- A welcome bot that greets new members with a custom embedded message, triggered by the
on_member_joinevent. - A reaction-role tool that automatically assigns roles when users react to a message, perfect for server verification or channel access.
- A custom command bot that fetches data from an external API—like weather or game stats—and displays it in a clean, formatted embed.
Simply describe your app, and Replit will write the code, test it, and fix issues automatically, all within your browser.
Common errors and challenges
Building a bot involves some common hurdles, but most issues have straightforward fixes once you know where to look.
Fixing the on_message event when commands stop working
If your commands suddenly stop working after you've added an on_message event, it's because you've overridden the bot's default message handling. The discord.py library uses its own internal on_message to listen for and dispatch commands. When you define your own, that internal process is ignored.
The fix is simple: you just need to tell your bot to continue looking for commands after your custom logic runs. Including await bot.process_commands(message) at the end of your on_message function ensures that messages are passed along for command processing, allowing both your event logic and your commands to work together.
Troubleshooting permission errors with discord.py
Permission-related errors, often appearing as discord.Forbidden, usually stem from one of two areas: your bot's server role or its declared intents.
- Server Permissions: Your bot is like any other user on your server and is governed by roles. If your bot needs to kick members or create channels, its role must have those specific permissions enabled in your server's settings under "Roles."
- Bot Intents: Intents are subscriptions to specific events from Discord. If you want your bot to access message content or track when members join, you must enable privileged intents in both your code—for example,
discord.Intents.all()—and in your bot's settings on the Discord Developer Portal.
Resolving issues with async/await in command functions
Many functions in discord.py are coroutines, which are special functions that don't run immediately. Instead of returning a value directly, they return an object that must be run by an event loop. This is where the async and await keywords come into play.
If a command seems to do nothing—no message sent, no error—you've likely forgotten to use await. For example, writing ctx.send("hello") won't actually send the message. You must write await ctx.send("hello") to pause your function, execute the send operation, and wait for it to complete. Forgetting this is a common mistake that causes code to fail silently.
Fixing the on_message event when commands stop working
When you define your own on_message event, you take full control over how the bot handles incoming messages. If your prefixed commands suddenly stop working, it’s because the bot is no longer processing them. The code below illustrates this common pitfall.
@bot.event
async def on_message(message):
if message.author == bot.user:
return
if 'hello' in message.content.lower():
await message.channel.send('Hello there!')
# Commands won't work without this lineBy defining a custom on_message event, you've taken over message handling. The function runs its logic but never tells the bot to check for commands, effectively ignoring them. The corrected code below adds the necessary line.
@bot.event
async def on_message(message):
if message.author == bot.user:
return
if 'hello' in message.content.lower():
await message.channel.send('Hello there!')
await bot.process_commands(message)The fix is simple: add await bot.process_commands(message) to the end of your on_message function. By default, defining this event overrides the bot's ability to process commands. This line hands control back, ensuring your prefixed commands still run after your custom message logic. You'll need this whenever you want your bot to both react to specific keywords and respond to commands, as it's easy to forget this final step.
Troubleshooting permission errors with discord.py
Permission errors, often appearing as discord.Forbidden, happen when your bot attempts an action without the right authorization. This is especially common with administrative commands, like a kick function, even when your code seems correct. The following example demonstrates this scenario.
@bot.command()
async def kick(ctx, member: discord.Member, *, reason=None):
await member.kick(reason=reason)
await ctx.send(f'Kicked {member.display_name}')The kick command itself is valid, but it requests a privileged action. If the bot's server role doesn't have 'Kick Members' permission, Discord will forbid the action, causing an error. The solution involves a two-part check.
@bot.command()
async def kick(ctx, member: discord.Member, *, reason=None):
try:
await member.kick(reason=reason)
await ctx.send(f'Kicked {member.display_name}')
except discord.Forbidden:
await ctx.send("I don't have permission to kick members.")The solution is to wrap the privileged action, await member.kick(), in a try...except block. This lets your bot attempt the command and handle failure gracefully instead of crashing. Learning the fundamentals of try and except in Python will help you implement proper error handling throughout your bot. You should watch for this error whenever your bot performs an action that requires special permissions, such as banning users or managing roles.
- The Fix: Catch the specific
discord.Forbiddenerror and send a user-friendly message explaining that the bot lacks permission.
Resolving issues with async/await in command functions
Many discord.py functions are asynchronous, meaning they won't run unless you use the await keyword. Forgetting it is a common mistake that causes commands to fail silently without any errors. The code below shows a typical example of this problem.
@bot.command()
def say_hello(ctx):
ctx.send("Hello!")Because the ctx.send() coroutine is never awaited, the command appears to run successfully but produces no output. The bot simply does nothing. The corrected code below shows how to properly execute the asynchronous function.
@bot.command()
async def say_hello(ctx):
await ctx.send("Hello!")The solution is to make the command function asynchronous by using async def and then calling the action with await. Functions like ctx.send() are coroutines—they don't run on their own. Forgetting await means the send operation is prepared but never actually executed, so your command does nothing. You'll need to use await for any discord.py function that performs an action, like sending messages or fetching user data.
Real-world applications
With the fundamentals and common errors covered, you can now build practical applications that automate and engage your community.
Creating a role reaction system with discord.py
A reaction-role system automates server management by letting users assign themselves roles, a feature you can build by handling the on_raw_reaction_add event. This approach to building chat bots demonstrates similar automation patterns across different platforms.
@bot.event
async def on_raw_reaction_add(payload):
if payload.message_id != 123456789: # Replace with your message ID
return
guild = bot.get_guild(payload.guild_id)
member = guild.get_member(payload.user_id)
if str(payload.emoji) == "🔴":
red_role = discord.utils.get(guild.roles, name="Red Team")
await member.add_roles(red_role)
elif str(payload.emoji) == "🔵":
blue_role = discord.utils.get(guild.roles, name="Blue Team")
await member.add_roles(blue_role)This code uses the on_raw_reaction_add event, which is powerful because it captures reactions on any message, even those not in the bot's cache. The function first checks if the reaction occurred on a specific message by comparing the payload.message_id. If it's the correct message, the bot takes action based on the emoji.
- It fetches the server and the specific member who reacted using IDs from the
payload. - It checks which emoji was used, such as
"🔴"or"🔵". - Finally, it finds a server role by its name—like "Red Team"—and assigns it to the member using
member.add_roles().
Implementing scheduled announcements with asyncio
You can create automated, recurring messages by using the discord.py tasks extension, which leverages asyncio to run functions like sending announcements on a set schedule.
import asyncio
import datetime
@tasks.loop(hours=24)
async def daily_announcement():
channel = bot.get_channel(987654321) # Announcement channel ID
weekday = datetime.datetime.now().strftime('%A')
if weekday == 'Monday':
await channel.send("It's Monday! Weekly team meeting at 10 AM.")
elif weekday == 'Friday':
await channel.send("Happy Friday! Don't forget to submit your weekly reports.")
else:
await channel.send(f"Good morning everyone! It's {weekday}.")
@bot.event
async def on_ready():
print(f'{bot.user} has connected to Discord!')
daily_announcement.start()The @tasks.loop decorator is a powerful tool for creating scheduled events without complex asyncio boilerplate. It wraps the daily_announcement function, setting it to run on a fixed interval. For more granular timing control, you might also need to understand using sleep in Python for custom delays. The function itself is straightforward—it grabs a channel, gets the current day using datetime, and sends a message based on conditional logic.
- Starting the task within
on_readyusingdaily_announcement.start()is critical. - This guarantees the bot is fully initialized before the loop attempts to fetch a channel and send its first message, preventing startup errors.
Get started with Replit
Now, turn these techniques into a working tool. Describe what you want to Replit Agent, like “a bot that assigns roles based on emoji reactions” or “a bot that sends scheduled daily announcements.”
The Agent will write the code, test for errors, and deploy your application for you. Start building with Replit.