NapNodes

← Discord bot guides

Discord bots · updated 23 October 2026

Discord Moderation Bot in Python: Warn, Mute, Kick, Ban and Purge Messages

A moderation bot does in one command what takes several clicks by hand, and it also records who did what. This one, in discord.py, has six slash commands: /warn (with a history that isn't lost on restart), /warnings, /mute, /kick, /ban and /purge, and it logs everything in a log channel. About 150 lines, no database and no special intents.

What the bot does and doesn't do

Each command is only visible to people with the permission that action needs (timing out, kicking, banning, managing messages): no need to set up extra roles. Before acting, the bot checks the role hierarchy, which is what fails most often: nobody can moderate someone at their level or above, neither the moderator nor the bot.

What it doesn't do is read the chat looking for swearing or spam. Discord already has AutoMod for that, in “Server Settings” → “AutoMod”: it filters words, links, spam and mass mentions without any bot, and it works even when yours is offline. A bot that reads every message needs the Message Content Intent and is more work for the same thing. The sensible split is AutoMod for filtering and this bot for what a person decides.

Rather not set it up yourself? Discord bot hosting from €1.50 a month, no contract and a full refund within 72 hours. Or create a free Discord bot, no card needed.

1. Create the bot on Discord

  1. Create the application and get the token

    At discord.com/developers/applications, “New Application”; in the “Bot” tab, “Reset Token” and copy the token. It's the bot's password: don't share it or push it to GitHub. If this is your first bot, the step-by-step process is in your first bot in Python.

  2. Invite it with the moderation permissions

    In “OAuth2” → “URL Generator”, tick “bot” and “applications.commands”, and for permissions “Send Messages”, “Embed Links”, “Read Message History”, “Manage Messages”, “Moderate Members”, “Kick Members” and “Ban Members”. the bot permissions calculator explains each one and gives you the link. Leave the “Privileged Gateway Intents” off.

  3. Put its role above regular members

    In “Server Settings” → “Roles”, drag the bot's role above the roles of the people it will moderate, and below the moderators' role. A bot can't time out, kick or ban someone whose highest role is at its level or above, even if it has the permission.

  4. (Optional) Create the log channel

    A private channel, for moderators only (for example #mod-log), where the bot records each action. Turn on “Developer Mode” in your Discord settings → “Advanced”, right-click the channel and “Copy Channel ID”: that number goes in the .env.

2. The code

Three files in one folder. The token and the log channel id go in .env, without quotes (if you don't want a log channel, delete that line). You need Python 3.10 or newer. Warnings are saved in warnings.json, which the bot creates by itself next to app.py.

requirements.txt
discord.py
python-dotenv
.env
DISCORD_TOKEN=paste-your-token-here
LOG_CHANNEL=123456789012345678
app.py
import datetime
import json
import os
from pathlib import Path

import discord
from discord import app_commands
from dotenv import load_dotenv

load_dotenv()  # reads the token and the log channel from the .env file

# The default intents are enough: slash commands arrive as interactions.
client = discord.Client(intents=discord.Intents.default())
tree = app_commands.CommandTree(client)

# Warnings are saved in a file next to the bot: they aren't lost on restart.
WARNINGS = Path("warnings.json")
LOG_CHANNEL = int(os.getenv("LOG_CHANNEL") or 0)


def load_warnings() -> dict:
    return json.loads(WARNINGS.read_text("utf-8")) if WARNINGS.exists() else {}


def save_warnings(data: dict) -> None:
    WARNINGS.write_text(json.dumps(data, ensure_ascii=False, indent=2), "utf-8")


def not_allowed(interaction: discord.Interaction, member: discord.Member) -> str | None:
    """Discord won't let you moderate someone at your level or above: we check it first."""
    guild = interaction.guild
    if member == interaction.user:
        return "You can't use it on yourself."
    if member.id == guild.owner_id:
        return "The server owner can't be moderated."
    if member.top_role >= guild.me.top_role:
        return f"{member.display_name}'s role is at my level or above: move mine up in “Server Settings” → “Roles”."
    if interaction.user.id != guild.owner_id and member.top_role >= interaction.user.top_role:
        return f"{member.display_name}'s role is at your level or above."
    return None


async def log(interaction: discord.Interaction, action: str, member: discord.Member, reason: str) -> None:
    """Records the action in the log channel, if there's one in the .env."""
    channel = client.get_channel(LOG_CHANNEL)
    if channel is None:
        return
    card = discord.Embed(title=action, color=0x5B4AE8, timestamp=discord.utils.utcnow())
    card.add_field(name="Member", value=f"{member.mention} ({member})")
    card.add_field(name="By", value=interaction.user.mention)
    card.add_field(name="Reason", value=reason, inline=False)
    await channel.send(embed=card)


async def dm(member: discord.Member, text: str) -> None:
    # Before kicking or banning: afterwards you can't message them any more.
    try:
        await member.send(text)
    except discord.HTTPException:
        pass  # they have DMs closed


@tree.command(name="warn", description="Gives a member a warning (it's recorded)")
@app_commands.guild_only()
@app_commands.default_permissions(moderate_members=True)
@app_commands.describe(member="Who", reason="Why")
async def warn(interaction: discord.Interaction, member: discord.Member, reason: str):
    if error := not_allowed(interaction, member):
        await interaction.response.send_message(error, ephemeral=True)
        return
    data = load_warnings()
    entries = data.setdefault(str(interaction.guild.id), {}).setdefault(str(member.id), [])
    entries.append({"reason": reason, "by": interaction.user.id, "date": discord.utils.utcnow().isoformat()})
    save_warnings(data)
    await interaction.response.send_message(f"⚠️ {member.mention}, you have a warning: {reason} (that's {len(entries)}).")
    await log(interaction, f"Warning #{len(entries)}", member, reason)


@tree.command(name="warnings", description="The warnings a member has")
@app_commands.guild_only()
@app_commands.default_permissions(moderate_members=True)
async def warnings(interaction: discord.Interaction, member: discord.Member):
    entries = load_warnings().get(str(interaction.guild.id), {}).get(str(member.id), [])
    if not entries:
        await interaction.response.send_message(f"{member.display_name} has no warnings.", ephemeral=True)
        return
    lines = [f"{n}. {w['date'][:10]} — {w['reason']} (<@{w['by']}>)" for n, w in enumerate(entries[-15:], 1)]
    await interaction.response.send_message(f"Warnings for {member.display_name} ({len(entries)}):\n" + "\n".join(lines), ephemeral=True)


@tree.command(name="mute", description="Times a member out: they can't type or talk")
@app_commands.guild_only()
@app_commands.default_permissions(moderate_members=True)
@app_commands.describe(minutes="From 1 minute to 28 days (40320 minutes)")
async def mute(interaction: discord.Interaction, member: discord.Member,
               minutes: app_commands.Range[int, 1, 40320], reason: str = "No reason"):
    if error := not_allowed(interaction, member):
        await interaction.response.send_message(error, ephemeral=True)
        return
    await member.timeout(datetime.timedelta(minutes=minutes), reason=reason)
    await interaction.response.send_message(f"🔇 {member.mention} muted for {minutes} min: {reason}")
    await log(interaction, f"Muted {minutes} min", member, reason)


@tree.command(name="kick", description="Removes a member (they can come back with another invite)")
@app_commands.guild_only()
@app_commands.default_permissions(kick_members=True)
async def kick(interaction: discord.Interaction, member: discord.Member, reason: str = "No reason"):
    if error := not_allowed(interaction, member):
        await interaction.response.send_message(error, ephemeral=True)
        return
    await dm(member, f"You've been kicked from {interaction.guild.name}: {reason}")
    await member.kick(reason=reason)
    await interaction.response.send_message(f"👢 {member} kicked: {reason}")
    await log(interaction, "Kicked", member, reason)


@tree.command(name="ban", description="Removes a member and doesn't let them back")
@app_commands.guild_only()
@app_commands.default_permissions(ban_members=True)
@app_commands.describe(delete_days="Delete their messages from the last few days (0 to 7)")
async def ban(interaction: discord.Interaction, member: discord.Member, reason: str = "No reason",
              delete_days: app_commands.Range[int, 0, 7] = 0):
    if error := not_allowed(interaction, member):
        await interaction.response.send_message(error, ephemeral=True)
        return
    await dm(member, f"You've been banned from {interaction.guild.name}: {reason}")
    await interaction.guild.ban(member, reason=reason, delete_message_seconds=delete_days * 86400)
    await interaction.response.send_message(f"🔨 {member} banned: {reason}")
    await log(interaction, "Banned", member, reason)


@tree.command(name="purge", description="Deletes the latest messages in this channel")
@app_commands.guild_only()
@app_commands.default_permissions(manage_messages=True)
async def purge(interaction: discord.Interaction, amount: app_commands.Range[int, 1, 100]):
    # Deleting many takes a while: first we tell Discord to wait.
    await interaction.response.defer(ephemeral=True)
    deleted = await interaction.channel.purge(limit=amount)
    await interaction.followup.send(f"Deleted {len(deleted)} messages.", ephemeral=True)


@tree.error
async def on_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
    original = getattr(error, "original", error)
    if isinstance(original, discord.Forbidden):
        text = "I'm missing a permission for that, or that person's role is above mine."
    else:
        text = "Something went wrong: check the bot's console."
        print(f"Error: {original!r}")
    if interaction.response.is_done():
        await interaction.followup.send(text, ephemeral=True)
    else:
        await interaction.response.send_message(text, ephemeral=True)


@client.event
async def on_ready():
    await tree.sync()  # publishes the six commands
    print(f"Logged in as {client.user}")


client.run(os.environ["DISCORD_TOKEN"])

3. Try it

Who sees each command can be changed without touching the code in “Server Settings” → “Integrations” → your bot: for example, leaving /ban only to the admin role and giving /warn to helpers too.

  1. Start the bot

    In a terminal, in the folder: pip install -r requirements.txt and python app.py. When it says “Logged in as…”, you're set. Commands can take a few minutes to show up the first time.

  2. Test it on a test account

    Invite a second account of yours (or a friend who doesn't mind) with no roles. With your account, /warn that account: the warning appears in the channel and, if you set LOG_CHANNEL, the card in the log. /warnings shows the history only to you.

  3. Mute for a minute

    /mute with 1 minute: the other account can't type or talk in voice until it's over. To lift it early, right-click the member and choose “Remove Timeout”.

  4. Clean up the channel

    /purge 10 deletes the last ten messages in the channel where you type it. Only people who can manage messages see it.

If something doesn't work

“…'s role is at my level or above”: the bot's role has to be higher than the highest role of the person being moderated. Move it up in “Server Settings” → “Roles”. That's why another admin or the server owner can't be moderated, neither with the bot nor by hand.

“I'm missing a permission for that”: one of the permissions on the list wasn't ticked when you invited it, or a specific channel takes it away. Generate the link again with the permissions above (no need to kick the bot) or check that channel's permissions.

/purge doesn't delete old messages in one go: Discord only allows bulk deletion of messages from the last 14 days; older ones discord.py deletes one by one, so it takes longer. If the commands don't show up, what to check when they don't appear; if the bot is online but doesn't reply, my bot isn't responding.

4. Keep it online all the time (for free)

A moderation bot that only works while your computer is on isn't much use: trouble tends to show up at night. It has to be hosted on a server.

On NapNodes: create the free bot (it starts out on Node.js; for this one, go to “Settings” → “Language” and choose “Switch to Python”), upload the three files in “Files” and click “Start”. The libraries in requirements.txt install themselves at startup and app.py is the startup file that comes set. warnings.json shows up in “Files” next to app.py: you can download or edit it from there.

The free bot has 256 MB, more than enough for a moderation bot, and you renew it with one click every week: if you forget, it stays off until you start it (and after 14 days without renewing it's deleted, with a warning first; keep a copy of warnings.json). So you don't have to remember, the Mini plan (512 MB) stays online with nothing to renew (Discord bot pricing). The whole setup is in the Discord bot hosting guide.

Ideas to extend it

Automatic punishments: after the third warning, mute for a day (in /warn, if len(entries) reaches 3, call member.timeout). An /unwarn command that removes the last one from the list. Also logging the messages AutoMod deletes, with the on_automod_action event (it needs the AutoMod execution intent; the intents tool tells you which). Or combine it with the welcome bot and the role bot in a single bot: all three fit in the free bot.

Frequently asked questions

How do I make a moderation bot for Discord?

With slash commands that call Discord's actions (timeout, kick, ban, delete messages) and check the role hierarchy first. The full code in Python, with warnings and a log channel, is in this guide.

What permissions does a moderation bot need?

Moderate Members to time out, Kick Members to kick, Ban Members to ban and Manage Messages with Read Message History to delete; plus Send Messages and Embed Links to reply. And its role has to be above the people it will moderate.

Why can't my bot ban someone?

It's almost always the hierarchy: that person's highest role is at the bot's role level or above. The server owner can never be banned.

Do I need a bot to filter swear words on Discord?

No. AutoMod, in “Server Settings” → “AutoMod”, filters words, spam and mass mentions without a bot. The bot is for what a person decides: warnings with history, muting, kicking and keeping a record.

Are warnings lost when the bot restarts?

No: they're saved in warnings.json, next to the bot. If you move it somewhere else, take that file with you.

Shall we set it up for you?

Discord bot hosting from €1.50 a month, never oversold: your RAM is yours alone. And if you'd rather start without paying, there's a free Discord bot, no card needed.

Free tool: Discord bot permissions calculator. Tick your bot's permissions and get the number and the invite link; or paste a number and see which permissions it grants.

Keep reading

All Discord bot guides, sorted →