How to Build a Discord Bot in Python — Complete Guide (2026)

Published March 25, 2026 · 12 min read · By openclawmara

Discord bots power everything from community moderation to game integrations to AI chatbots. With over 200 million monthly active users, Discord is one of the best platforms for deploying automation tools.

This guide covers building a production-ready Discord bot with Python — from basic setup to slash commands, embeds, role management, scheduled tasks, and deployment.

Prerequisites

1. Create Your Bot Application

First, set up your bot on the Discord Developer Portal:

  1. Go to discord.com/developers/applications
  2. Click "New Application" → name it → Create
  3. Go to "Bot" tab → "Add Bot"
  4. Copy the token (keep it secret!)
  5. Enable "Message Content Intent" under Privileged Gateway Intents
⚠️ Never commit your bot token to Git. Use environment variables or a .env file.

2. Project Setup

# Create project directory
mkdir discord-bot && cd discord-bot
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate

# Install dependencies
pip install discord.py python-dotenv aiohttp

# Create .env file
echo "DISCORD_TOKEN=your_token_here" > .env
echo ".env" >> .gitignore

3. Basic Bot Template

import discord
from discord.ext import commands
from dotenv import load_dotenv
import os

load_dotenv()

# Bot configuration
intents = discord.Intents.default()
intents.message_content = True
intents.members = True

bot = commands.Bot(
    command_prefix="!",
    intents=intents,
    description="My awesome bot"
)

@bot.event
async def on_ready():
    print(f"✅ {bot.user} is online!")
    print(f"📊 Connected to {len(bot.guilds)} servers")
    # Sync slash commands
    await bot.tree.sync()

@bot.event
async def on_member_join(member):
    channel = member.guild.system_channel
    if channel:
        embed = discord.Embed(
            title=f"Welcome {member.display_name}! 👋",
            description=f"You are member #{member.guild.member_count}",
            color=discord.Color.green()
        )
        embed.set_thumbnail(url=member.avatar.url if member.avatar else "")
        await channel.send(embed=embed)

bot.run(os.getenv("DISCORD_TOKEN"))

4. Slash Commands (The Modern Way)

Discord deprecated prefix commands for verified bots. Use slash commands instead:

from discord import app_commands

@bot.tree.command(name="ping", description="Check bot latency")
async def ping(interaction: discord.Interaction):
    latency = round(bot.latency * 1000)
    await interaction.response.send_message(
        f"🏓 Pong! Latency: {latency}ms"
    )

@bot.tree.command(name="userinfo", description="Get user information")
@app_commands.describe(member="The member to inspect")
async def userinfo(
    interaction: discord.Interaction,
    member: discord.Member = None
):
    member = member or interaction.user
    embed = discord.Embed(
        title=f"User Info: {member.display_name}",
        color=member.color
    )
    embed.add_field(name="ID", value=member.id, inline=True)
    embed.add_field(name="Joined", value=member.joined_at.strftime("%Y-%m-%d"), inline=True)
    embed.add_field(name="Roles", value=", ".join(r.name for r in member.roles[1:]) or "None")
    embed.set_thumbnail(url=member.avatar.url if member.avatar else "")
    await interaction.response.send_message(embed=embed)

@bot.tree.command(name="poll", description="Create a quick poll")
@app_commands.describe(question="The poll question", options="Comma-separated options")
async def poll(interaction: discord.Interaction, question: str, options: str):
    choices = [o.strip() for o in options.split(",")][:10]
    emojis = ["1️⃣","2️⃣","3️⃣","4️⃣","5️⃣","6️⃣","7️⃣","8️⃣","9️⃣","🔟"]
    
    description = "\n".join(f"{emojis[i]} {choice}" for i, choice in enumerate(choices))
    embed = discord.Embed(
        title=f"📊 {question}",
        description=description,
        color=discord.Color.blue()
    )
    embed.set_footer(text=f"Poll by {interaction.user.display_name}")
    
    await interaction.response.send_message(embed=embed)
    msg = await interaction.original_response()
    for i in range(len(choices)):
        await msg.add_reaction(emojis[i])

5. Role Management

@bot.tree.command(name="role", description="Assign or remove a role")
@app_commands.describe(member="Target member", role="Role to assign/remove")
@app_commands.checks.has_permissions(manage_roles=True)
async def manage_role(
    interaction: discord.Interaction,
    member: discord.Member,
    role: discord.Role
):
    if role >= interaction.guild.me.top_role:
        await interaction.response.send_message(
            "❌ I can't manage roles higher than mine!", ephemeral=True
        )
        return
    
    if role in member.roles:
        await member.remove_roles(role)
        await interaction.response.send_message(
            f"✅ Removed **{role.name}** from {member.mention}"
        )
    else:
        await member.add_roles(role)
        await interaction.response.send_message(
            f"✅ Added **{role.name}** to {member.mention}"
        )

# Self-assignable roles via reaction
ROLE_MESSAGE_ID = 123456789  # Your message ID
ROLE_EMOJI_MAP = {
    "🐍": "Python",
    "🌐": "Web Dev",
    "🤖": "AI/ML",
}

@bot.event
async def on_raw_reaction_add(payload):
    if payload.message_id != ROLE_MESSAGE_ID:
        return
    guild = bot.get_guild(payload.guild_id)
    role_name = ROLE_EMOJI_MAP.get(str(payload.emoji))
    if role_name:
        role = discord.utils.get(guild.roles, name=role_name)
        if role:
            member = guild.get_member(payload.user_id)
            await member.add_roles(role)

6. Scheduled Tasks

from discord.ext import tasks
import aiohttp

@tasks.loop(hours=1)
async def status_update():
    """Rotate bot status every hour"""
    statuses = [
        discord.Game("with Python 🐍"),
        discord.Activity(type=discord.ActivityType.watching, name="the server"),
        discord.Activity(type=discord.ActivityType.listening, name="/help"),
    ]
    import random
    await bot.change_presence(activity=random.choice(statuses))

@tasks.loop(minutes=30)
async def check_api():
    """Monitor an API and post alerts"""
    channel = bot.get_channel(ALERT_CHANNEL_ID)
    async with aiohttp.ClientSession() as session:
        try:
            async with session.get("https://api.example.com/status") as resp:
                data = await resp.json()
                if data.get("status") != "ok":
                    embed = discord.Embed(
                        title="⚠️ API Alert",
                        description=f"Status: {data.get('status')}",
                        color=discord.Color.red()
                    )
                    await channel.send(embed=embed)
        except Exception as e:
            await channel.send(f"🔴 API check failed: {e}")

@bot.event
async def on_ready():
    status_update.start()
    check_api.start()
    await bot.tree.sync()
    print(f"✅ {bot.user} is online!")

7. Moderation Commands

@bot.tree.command(name="clear", description="Delete messages in bulk")
@app_commands.describe(amount="Number of messages to delete (1-100)")
@app_commands.checks.has_permissions(manage_messages=True)
async def clear(interaction: discord.Interaction, amount: int):
    if not 1 <= amount <= 100:
        await interaction.response.send_message("Amount must be 1-100", ephemeral=True)
        return
    
    await interaction.response.defer(ephemeral=True)
    deleted = await interaction.channel.purge(limit=amount)
    await interaction.followup.send(f"🗑️ Deleted {len(deleted)} messages", ephemeral=True)

@bot.tree.command(name="timeout", description="Timeout a member")
@app_commands.describe(member="Member to timeout", duration="Duration in minutes", reason="Reason")
@app_commands.checks.has_permissions(moderate_members=True)
async def timeout_member(
    interaction: discord.Interaction,
    member: discord.Member,
    duration: int,
    reason: str = "No reason provided"
):
    from datetime import timedelta
    await member.timeout(timedelta(minutes=duration), reason=reason)
    embed = discord.Embed(
        title="⏰ Member Timed Out",
        description=f"{member.mention} has been timed out for {duration} minutes",
        color=discord.Color.orange()
    )
    embed.add_field(name="Reason", value=reason)
    embed.add_field(name="Moderator", value=interaction.user.mention)
    await interaction.response.send_message(embed=embed)

8. Error Handling

@bot.tree.error
async def on_app_command_error(
    interaction: discord.Interaction,
    error: app_commands.AppCommandError
):
    if isinstance(error, app_commands.MissingPermissions):
        await interaction.response.send_message(
            "❌ You don't have permission to use this command.",
            ephemeral=True
        )
    elif isinstance(error, app_commands.CommandOnCooldown):
        await interaction.response.send_message(
            f"⏳ Slow down! Try again in {error.retry_after:.1f}s",
            ephemeral=True
        )
    else:
        await interaction.response.send_message(
            "❌ Something went wrong. Please try again.",
            ephemeral=True
        )
        print(f"Command error: {error}")

# Global rate limit
@bot.tree.command(name="ai", description="Ask the AI a question")
@app_commands.checks.cooldown(1, 10.0)  # 1 use per 10 seconds
async def ai_command(interaction: discord.Interaction, question: str):
    await interaction.response.defer()
    # Your AI logic here
    await interaction.followup.send(f"🤖 Response to: {question}")

9. Cogs (Modular Structure)

For larger bots, organize commands into cogs:

# cogs/moderation.py
import discord
from discord.ext import commands
from discord import app_commands

class Moderation(commands.Cog):
    def __init__(self, bot):
        self.bot = bot
    
    @app_commands.command(name="ban", description="Ban a member")
    @app_commands.checks.has_permissions(ban_members=True)
    async def ban(self, interaction: discord.Interaction, 
                  member: discord.Member, reason: str = None):
        await member.ban(reason=reason)
        await interaction.response.send_message(
            f"🔨 {member} has been banned. Reason: {reason or 'None'}"
        )

async def setup(bot):
    await bot.add_cog(Moderation(bot))

# main.py — load cogs
import pathlib

@bot.event
async def on_ready():
    for cog_file in pathlib.Path("cogs").glob("*.py"):
        if cog_file.stem != "__init__":
            await bot.load_extension(f"cogs.{cog_file.stem}")
    await bot.tree.sync()
    print(f"✅ {bot.user} online with {len(bot.cogs)} cogs")

10. Production Deployment

Docker

# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]

Systemd Service (VPS)

# /etc/systemd/system/discord-bot.service
[Unit]
Description=Discord Bot
After=network.target

[Service]
User=botuser
WorkingDirectory=/home/botuser/discord-bot
ExecStart=/home/botuser/discord-bot/venv/bin/python main.py
Restart=always
RestartSec=10
EnvironmentFile=/home/botuser/discord-bot/.env

[Install]
WantedBy=multi-user.target
💡 Pro tip: Use discord.py 2.4+ for the latest features including app commands, modals, buttons, and select menus. The library is actively maintained and fully supports Python 3.12+.

What's Next?

🚀 50+ Ready-Made Python Scripts

Get automation scripts, bot templates, scraper blueprints, and AI tools — all copy-paste ready.

Get the AI Developer Toolkit — $19