Discord Bot Events in Python: Real-Time Listeners
Events are asynchronous notifications fired by Discord when server state changes—a message is posted, a user joins, a reaction is added, or a role is assigned. Discord bot events in discord.py are defined as async functions decorated with @bot.event that listen for specific triggers and execute custom logic. Unlike commands (which wait for user input), events fire automatically when their condition occurs, enabling bots to respond to server activity in real-time without explicit user instruction.
The on_message Event: The Foundation
The on_message event fires every time a message is posted in any channel the bot can see. This is the most frequently fired event and the basis for message-based automation:
import discord
from discord.ext import commands
import os
from dotenv import load_dotenv
load_dotenv()
bot = commands.Bot(command_prefix='!', intents=discord.Intents.default())
@bot.event
async def on_message(message):
"""Handle every message posted in channels the bot can see."""
# Ignore messages from the bot itself to prevent loops
if message.author == bot.user:
return
# Log incoming messages
print(f'{message.author}: {message.content}')
# Respond to a specific phrase
if 'hello bot' in message.content.lower():
await message.channel.send(f'Hello, {message.author.mention}!')
# CRITICAL: process commands after custom event logic
await bot.process_commands(message)
bot.run(os.getenv('DISCORD_TOKEN'))
The on_message function receives a Message object with author, content, channel, guild, and timestamp. Always check if message.author == bot.user: return to avoid the bot responding to itself infinitely. After your custom logic, call await bot.process_commands(message) to allow commands (decorated with @bot.command()) to still work; without this line, on_message intercepts all messages and commands never execute.
Member Events: Welcoming and Tracking Users
Track when users join or leave the server with on_member_join and on_member_remove. These events require the GUILD_MEMBERS privileged intent:
# Enable GUILD_MEMBERS intent in your bot initialization
intents = discord.Intents.default()
intents.members = True
bot = commands.Bot(command_prefix='!', intents=intents)
@bot.event
async def on_member_join(member):
"""Welcome a member when they join."""
# Send a DM to the new member
try:
await member.send(f'Welcome to {member.guild.name}! Read the rules in #rules.')
except discord.Forbidden:
# Member has DMs disabled
pass
# Post in a welcome channel
welcome_channel = discord.utils.get(member.guild.channels, name='welcome')
if welcome_channel:
embed = discord.Embed(
title='New Member!',
description=f'{member.mention} has joined the server.',
color=discord.Color.green()
)
await welcome_channel.send(embed=embed)
@bot.event
async def on_member_remove(member):
"""Log when a member leaves."""
log_channel = discord.utils.get(member.guild.channels, name='logs')
if log_channel:
await log_channel.send(f'{member.mention} ({member.name}) has left the server.')
on_member_join fires after a user joins; on_member_remove fires when they leave or are kicked. Use member.guild to get the guild object, member.guild.channels to iterate channels, and discord.utils.get() to find a channel by name. Wrap member.send() in a try-except because users can disable DMs.
Reaction Events: Interactive Voting and Polls
Reactions (emoji) can trigger actions. on_reaction_add fires when a user adds a reaction; on_reaction_remove fires when a reaction is removed:
@bot.event
async def on_reaction_add(reaction, user):
"""Handle reaction additions."""
# Ignore bot reactions
if user == bot.user:
return
message = reaction.message
emoji = str(reaction.emoji)
# Example: role-based emoji reactions
if message.guild and emoji == '👍':
role = discord.utils.get(message.guild.roles, name='Approved')
if role:
try:
await user.add_roles(role)
print(f'{user.name} reacted with {emoji}, added Approved role')
except discord.Forbidden:
print('Bot lacks permission to assign roles')
if emoji == '👎':
role = discord.utils.get(message.guild.roles, name='Pending')
if role:
try:
await user.remove_roles(role)
print(f'{user.name} removed Pending role')
except discord.Forbidden:
pass
@bot.event
async def on_reaction_remove(reaction, user):
"""Handle reaction removals."""
if user == bot.user:
return
emoji = str(reaction.emoji)
if emoji == '👍':
role = discord.utils.get(reaction.message.guild.roles, name='Approved')
if role:
try:
await user.remove_roles(role)
print(f'{user.name} removed {emoji}, removed Approved role')
except discord.Forbidden:
pass
This example assigns/removes roles when users react. reaction.emoji is the emoji object; convert to string for comparison. Check message.guild to ensure this is a guild message (not a DM). Use user.add_roles() and user.remove_roles() to modify roles; wrap in try-except because the bot needs appropriate permissions.
Ready Event: Initialization and Status Updates
The on_ready event fires when the bot successfully logs in and is fully initialized. Use it to set the bot's status and log startup:
@bot.event
async def on_ready():
"""Fires when the bot is ready."""
print(f'{bot.user} is now online.')
# Set a custom status
activity = discord.Activity(type=discord.ActivityType.watching, name='Python tutorials')
await bot.change_presence(activity=activity)
# Log ready timestamp
import datetime
timestamp = datetime.datetime.now().isoformat()
print(f'Bot ready at {timestamp}')
bot.change_presence() updates the bot's status visible in the member list. ActivityType can be playing, streaming, listening, watching, or competing. on_ready fires once after login and again if the bot reconnects after a network outage.
Guild Events: Monitoring Server Changes
Track when channels are created, roles are modified, or other guild-level changes occur:
@bot.event
async def on_guild_channel_create(channel):
"""Log when a new channel is created."""
log_channel = discord.utils.get(channel.guild.channels, name='logs')
if log_channel:
await log_channel.send(f'Channel `#{channel.name}` was created.')
@bot.event
async def on_guild_channel_delete(channel):
"""Log when a channel is deleted."""
log_channel = discord.utils.get(channel.guild.channels, name='logs')
if log_channel:
await log_channel.send(f'Channel `#{channel.name}` was deleted.')
@bot.event
async def on_member_update(before, after):
"""Track when a member's roles or profile change."""
# Check if roles changed
before_roles = set(before.roles)
after_roles = set(after.roles)
if before_roles != after_roles:
added = after_roles - before_roles
removed = before_roles - after_roles
log_channel = discord.utils.get(before.guild.channels, name='logs')
if log_channel:
if added:
role_names = ', '.join([r.name for r in added])
await log_channel.send(f'{after.mention} was given: {role_names}')
if removed:
role_names = ', '.join([r.name for r in removed])
await log_channel.send(f'{after.mention} lost: {role_names}')
on_guild_channel_create and on_guild_channel_delete monitor channel lifecycle. on_member_update receives a before and after state; compare roles, nickname, or other attributes to detect changes. These events enable detailed audit logging.
Event Ordering and Best Practices
Events fire in order, and multiple events can trigger from a single action. For example, a user receiving a role may fire on_member_update and then on_raw_role_update. Keep event handlers lightweight; heavy computation blocks the bot from processing other events. For long-running tasks, dispatch them to a background task or use the task scheduler covered in article 7.
| Event | Fires when | Common use |
|---|---|---|
on_message | Any message is posted | Keyword detection, profanity filtering |
on_member_join | User joins the guild | Welcome messages, automatic roles |
on_reaction_add | User reacts to a message | Role assignment, poll voting |
on_guild_channel_create | Channel is created | Audit logging |
on_member_update | Member roles or profile change | Role tracking, moderation logs |
on_ready | Bot connects and initializes | Status setting, startup logs |
Key Takeaways
- Define events as async functions decorated with
@bot.event, named after the Discord event (e.g.,on_message,on_member_join). - The
on_messageevent fires for every message; always checkif message.author == bot.user: returnto avoid loops, and callawait bot.process_commands(message)at the end to allow commands to still work. on_member_joinandon_member_removetrack user lifecycle; enable theGUILD_MEMBERSintent to access them.- Reaction events (
on_reaction_add,on_reaction_remove) enable interactive features like role assignment via emoji. - Guild events (
on_guild_channel_create,on_member_update) monitor server state changes for moderation and auditing. - Keep event handlers fast; dispatch heavy work to background tasks to prevent blocking the event loop.
Frequently Asked Questions
How do I enable the GUILD_MEMBERS intent?
In the Developer Portal, go to Bot and toggle on Privileged Gateway Intents > Server Members Intent. In code, set intents.members = True before creating the bot: intents = discord.Intents.default(); intents.members = True.
What is the difference between on_message and on_message_edit?
on_message fires when a new message is posted. on_message_edit fires when an existing message is edited. Use on_message_edit to detect and respond to message updates, e.g., to enforce word filters on edited messages.
Can I have a command and an on_message handler for the same content?
Yes, but on_message runs first. If on_message does something and then calls await bot.process_commands(message), both the on_message logic and the command execute. If on_message returns early without calling process_commands, the command never runs.
How do I ignore messages from specific bots or users?
In your event handler, add a check: if message.author.bot: return to ignore all bot messages, or if message.author.id == SPECIFIC_USER_ID: return to ignore a specific user.
Can events run concurrently, or do they queue?
Events run sequentially in the order they are fired by Discord. If one event handler is slow, subsequent events queue. Use background tasks (asyncio.create_task() or the task scheduler) to avoid blocking.