Only show guides for your app
The Python version of the welcome bot — greet joiners with an embed, and enable the Server Members intent both in code and in the Developer Portal.
This is the discord.js welcome bot rewritten for discord.py: an embed posted in your welcome channel whenever someone joins. Python needs the same privileged intent, enabled in two places — the toggle in the Developer Portal and a line in your code.
| At a glance | |
|---|---|
| You need | a working bot from Host a discord.py bot, and a channel to post welcomes in |
| Plan | Free or premium |
| Time | About fifteen minutes |
Build the discord.py starter first if you haven't — you need a bot with your token in .env on a server running the Python application.
Member-join events are gated behind a privileged intent. discord.py won't even connect if your code asks for one that isn't enabled in the portal.
⚠️ Heads up — this is a privileged intent. In the Discord Developer Portal, open your application → Bot → Privileged Gateway Intents → turn on Server Members Intent → save. The code sets
intents.members = True; if the portal toggle is off, the bot raisesPrivilegedIntentsRequiredat startup instead of running. Bots in more than 100 servers must be verified before Discord grants this intent.
The complete app.py. It reads the welcome channel's ID from .env:
import os
import discord
from dotenv import load_dotenv
load_dotenv()
intents = discord.Intents.default()
intents.members = True # Server Members intent — privileged
client = discord.Client(intents=intents)
WELCOME_CHANNEL_ID = int(os.environ["WELCOME_CHANNEL_ID"])
@client.event
async def on_ready():
print(f"Listening as {client.user}", flush=True)
@client.event
async def on_member_join(member: discord.Member):
channel = member.guild.get_channel(WELCOME_CHANNEL_ID)
if channel is None:
return
embed = discord.Embed(
title="👋 Welcome!",
description=f"Hey {member.mention}, welcome to **{member.guild.name}**!",
color=0x57F287,
)
embed.set_thumbnail(url=member.display_avatar.url)
embed.add_field(name="Member", value=str(member), inline=True)
embed.add_field(name="You are member #", value=str(member.guild.member_count), inline=True)
embed.timestamp = discord.utils.utcnow()
await channel.send(content=member.mention, embed=embed)
client.run(os.environ["DISCORD_TOKEN"])
Add the channel ID to .env alongside the token:
DISCORD_TOKEN=your-token
WELCOME_CHANNEL_ID=123456789012345678
Get the ID by enabling Developer Mode in Discord (User Settings → Advanced), then right-clicking the channel and choosing Copy Channel ID. Save and restart.
intents.members = True is the code half of the privileged intent. It has to be paired with the portal toggle, or discord.py refuses to start.int(os.environ["WELCOME_CHANNEL_ID"]) — channel IDs come out of .env as strings, and get_channel wants an integer, so the int(...) conversion is required.on_member_join is discord.py's join event. Its member argument gives you member.guild, member.mention, and member.display_avatar.member.mention placed in content (not the embed) is what actually pings the new member — mentions buried inside an embed don't notify anyone.discord.utils.utcnow() is the timezone-aware "now" discord.py expects for embed.timestamp; a plain datetime.now() warns about missing timezone info.member.guild.system_channel to post wherever Discord already routes system messages — no ID to configure.await member.send("Welcome!") messages them privately. Wrap it in try/except discord.Forbidden — people who block server DMs will raise it.on_member_remove event uses the same member object and the same intent.PrivilegedIntentsRequired at startup — the Server Members Intent is off in the portal while your code requests it. Enable it (see the callout) and restart. More in Bot appears offline.WELCOME_CHANNEL_ID points at the wrong channel. Re-copy the ID and confirm the bot can post there.KeyError: 'WELCOME_CHANNEL_ID' — the variable isn't in .env, or load_dotenv() runs after you read it. Keep load_dotenv() at the top.content, which the code does with content=member.mention.