How to make a Discord bot in Python
Learn how to make a Discord bot in Python. Explore different methods, tips, real-world uses, and how to debug common errors.

A custom Discord bot built with Python automates tasks and enhances community engagement. Python's libraries offer a powerful yet direct path to bring your unique bot ideas to life.
In this guide, you'll find essential techniques, practical tips, and real-world applications. You'll also get debugging advice to help you build and maintain a creative bot from the ground up.
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')--OUTPUT--MyBot#1234 has connected to Discord!
This initial code lays the groundwork for your bot using the discord.py library. It creates a bot instance, defines what happens upon a successful connection, and then runs it. Think of it as the bot's central nervous system.
Here’s what the key parts are doing:
- The
commands.Botclass is the heart of your bot. You define acommand_prefix—in this case,'!'—which tells the bot what character to look for at the start of a command. Theintentsparameter is crucial for declaring which events, like messages or member joins, your bot needs to listen for. - The
@bot.eventdecorator registers theon_readyfunction to handle a specific event. This function executes as soon as the bot connects to Discord, providing a clear confirmation that it's online and ready.
Core bot functionality
With the basic setup complete, you can bring your bot to life by responding to messages, creating custom commands with @bot.command(), and using rich embeds.
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)--OUTPUT--User: hello
Bot: Hello there!
The on_message event function runs for every message the bot can see. The initial if message.author == bot.user: check is a crucial safeguard; it prevents the bot from responding to its own messages and creating an infinite loop. This function lets you build custom interactions beyond simple commands.
- The logic checks if the message content, converted to lowercase with
message.content.lower(), contains 'hello' and sends a reply if it does. - The final line,
await bot.process_commands(message), is vital. It tells the library to also process any defined commands, ensuring both your custom message responses and your command-based functions work together.
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)--OUTPUT--User: !ping
Bot: Pong! Latency: 42ms
User: !echo Hello world
Bot: Hello world
The @bot.command() decorator is your primary tool for building commands. It links a function directly to a user-callable command, like !ping. The function receives a ctx (context) object, which contains useful information about the message and lets you send a reply with ctx.send().
- The
pingcommand shows a simple response, while theechocommand demonstrates how to capture user arguments. - The asterisk (
*) in theechofunction's parameters is a key detail. It tells the function to treat all text following the command as a single argument, making it perfect for commands that need to handle multi-word input.
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)--OUTPUT--[An embedded message appears with the title "Bot Info", description "A cool Discord bot", and fields for Author and Version]
Embeds let you send richly formatted messages that stand out. The discord.Embed class is your main tool for creating these structured blocks of content. You can initialize an embed with a title, description, and a hex color code.
- Use the
add_field()method to add organized information like author or version details. - The
inlineparameter is a simple but powerful layout tool. Setting it toTrueplaces fields side by side, whileFalsegives each field its own line.
Finally, you pass the entire embed object to ctx.send() to display it in the channel.
Advanced bot development
Beyond basic commands, you can create a more sophisticated bot by managing various Discord events, organizing your code into Cogs, and adding robust error handling.
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!')--OUTPUT--[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 handle a wide range of server activities automatically, making your bot feel more integrated and alive.
- The
on_member_joinevent triggers when a new user arrives. The function receives amemberobject, allowing you to grab the system channel and send a personalized welcome message. - Similarly,
on_reaction_addactivates when someone adds an emoji. The code checks that theuserisn't the bot and that the emoji is '👍' 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))--OUTPUT--[In main file after adding]
await bot.load_extension('greetings')
User: !hello
Bot: Hello, JohnDoe!
As your bot grows, Cogs help you keep the code organized. They are Python classes that inherit from commands.Cog, letting you group related commands and listeners into separate files called extensions. This approach prevents your main file from becoming cluttered.
- The class is initialized with the
botinstance, giving it access to the main bot's functionality. - You define commands inside the class using the familiar
@commands.command()decorator. - A required
setupfunction usesbot.add_cog()to register the Cog, making it active when you load the extension from your main script.
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)}')--OUTPUT--User: !echo
Bot: Please provide all required arguments.
User: !unknown
Bot: Command not found.
[In console/log file]
ERROR:root:Error: Something went wrong
Robust error handling keeps your bot from crashing and provides clear feedback to users. The on_command_error event function acts as a safety net, catching exceptions that occur when a command is run. This lets you manage errors gracefully instead of letting them fail silently.
- The code uses
isinstance()to check the specific type of error that occurred. - For common user mistakes like a
commands.MissingRequiredArgumentorcommands.CommandNotFound, the bot sends a helpful, user-friendly message in the channel. - All other unexpected errors are logged for you to debug later, keeping the chat clean.
Move faster with Replit
Replit is an AI-powered development platform that transforms natural language into working applications. You can describe what you want to build, and Replit Agent creates it—complete with databases, APIs, and deployment.
For the Discord bot techniques covered in this guide, Replit Agent can turn them into production-ready applications:
- Build a moderation bot that uses the
on_member_joinevent to assign roles andon_messageto flag keywords. - Create a server utility that uses a custom
@bot.command()to generate interactive polls withdiscord.Embedand tracks votes withon_reaction_add. - Deploy a modular information bot that organizes different API commands into separate
Cogsfor clean, scalable code.
Describe your bot idea, and Replit Agent writes the code, tests it, and fixes issues automatically. Try Replit Agent and turn your concept into a working application, all in your browser.
Common errors and challenges
Building a bot involves navigating a few common hurdles, but most have straightforward solutions that will strengthen your understanding of the library.
Fixing the on_message event when commands stop working
If your commands suddenly stop working after you add an on_message event, it’s because you’ve overridden the bot's default message handler. The library no longer automatically looks for commands. You must explicitly tell it to do so by adding await bot.process_commands(message) to the end of your on_message function, ensuring both your custom logic and commands can run together.
Troubleshooting permission errors with discord.py
Permission issues often manifest as your bot silently failing to respond to events. For your bot to "see" certain activities—like members joining or message content—it needs the correct "Intents" enabled. You must configure these permissions both in your code when initializing the bot and in your application's settings on the Discord Developer Portal.
Resolving issues with async/await in command functions
Since discord.py is an asynchronous library, nearly every action that involves waiting for Discord's API is a coroutine that needs the await keyword. A common mistake is calling a function like ctx.send() without await. This creates the task but never runs it, so your bot does nothing, and you may get a RuntimeWarning in your console.
Fixing the on_message event when commands stop working
It's a classic discord.py puzzle: you add a custom on_message event handler, and suddenly your commands go silent. This happens because the new event listener overrides the bot's default command processing. 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 line
The custom on_message function intercepts every message, but it lacks an instruction to pass them on for command processing. This effectively makes the bot ignore any defined commands. The corrected code below adds the required step.
@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 yet crucial. By adding await bot.process_commands(message), you tell the bot to continue checking for commands after your custom on_message logic runs. Without it, your event handler consumes the message, and the bot never knows to look for a command. Remember to include this line whenever you create a custom message listener; it ensures your commands and event-driven responses can coexist without conflict.
Troubleshooting permission errors with discord.py
When a command fails silently or throws a Forbidden error, the problem often isn't your code. Instead, it's a permissions issue. Your bot needs the right server roles to execute administrative actions, as the following code demonstrates.
@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 fails because it attempts a privileged action without confirming the bot has the required "Kick Members" permission. The following code demonstrates how to properly handle this check before executing the command.
@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 wraps the kick action in a try...except block, letting you gracefully handle a discord.Forbidden error. This error occurs when your bot lacks the necessary server permissions for a privileged action.
Instead of crashing, the bot catches the exception and sends a clear message. It's a crucial pattern for any command that performs administrative tasks like kicking, banning, or managing roles, as it provides helpful feedback to the user.
Resolving issues with async/await in command functions
Since discord.py is an asynchronous library, forgetting to use await is a frequent pitfall. This mistake often leads to your bot doing nothing without obvious errors, leaving you wondering why the command failed. The code below shows this common oversight.
@bot.command()
def say_hello(ctx):
ctx.send("Hello!")
The say_hello function calls ctx.send() but doesn't wait for the action to complete. This creates a task that is never executed, so the message is never sent. The corrected code below shows how to resolve this.
@bot.command()
async def say_hello(ctx):
await ctx.send("Hello!")
The solution is to declare the function with async def and call ctx.send() with await. Since discord.py is asynchronous, any function that communicates with Discord's API is a coroutine. You must use await to execute it.
Forgetting this keyword creates the task but never runs it, so your bot appears to do nothing. It's a common issue, so watch for it whenever you see a RuntimeWarning in your console.
Real-world applications
With the fundamentals and common fixes covered, you can now build practical features like role reaction systems and scheduled announcements.
Creating a role reaction system with discord.py
You can create a role reaction system to automate server organization, allowing users to assign themselves roles by reacting to a message with an emoji.
@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. It's crucial because it captures reactions even on messages not in the bot's cache, making it reliable for persistent role-assignment messages.
- First, it filters reactions to a single, specific message using its
payload.message_id. - Next, it fetches the full server and member objects from the raw data in the
payload. - Finally, it checks the emoji, finds the matching role by name with
discord.utils.get(), and assigns it to the member usingmember.add_roles().
Implementing scheduled announcements with asyncio
The tasks extension in discord.py lets you automate recurring messages, such as daily announcements, by creating loops that run at fixed intervals.
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()
This code creates an automated daily message using the @tasks.loop decorator. This powerful tool schedules the daily_announcement function to run automatically every 24 hours.
- The function checks the current day of the week and sends a custom message to a specific channel.
- Crucially, the loop is activated with
daily_announcement.start()inside theon_readyevent. This ensures the scheduled task begins only after the bot successfully connects to Discord.
Get started with Replit
Turn these techniques into a real tool. Describe what you want to build, like a bot that "creates interactive polls using embeds and emoji reactions" or one that "welcomes new members and flags specific keywords in messages."
Replit Agent writes the code, tests for errors, and deploys your application automatically. Start building with Replit and bring your bot idea to life.
Create and deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.
Create & deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.


.png)
.png)