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.
Turn on the Server Members intent first
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 code
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.
The parts that matter
intents.members = Trueis 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.envas strings, andget_channelwants an integer, so theint(...)conversion is required.on_member_joinis discord.py's join event. Itsmemberargument gives youmember.guild,member.mention, andmember.display_avatar.member.mentionplaced incontent(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 forembed.timestamp; a plaindatetime.now()warns about missing timezone info.
Making it yours
- Use the system channel. Replace the lookup with
member.guild.system_channelto post wherever Discord already routes system messages — no ID to configure. - DM the newcomer.
await member.send("Welcome!")messages them privately. Wrap it intry/except discord.Forbidden— people who block server DMs will raise it. - Add a leave message. An
on_member_removeevent uses the same member object and the same intent. - Assign a role too. With the intent already on, Give every new member a role is a small addition to this handler.
Troubleshooting
PrivilegedIntentsRequiredat 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.- Bot runs but never welcomes anyone — the intent isn't really enabled, or
WELCOME_CHANNEL_IDpoints 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, orload_dotenv()runs after you read it. Keepload_dotenv()at the top.- The message posts but doesn't ping — the mention must be in
content, which the code does withcontent=member.mention.