aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Discord Bots

discord.py Cogs: Build a Modular, Clean Bot

Once a Discord bot starts to grow, a single main.py file quickly spirals out of control — and that is exactly where the discord.py cog pattern comes in. Cogs are modules that group related commands, event listeners and state under one class. By splitting each feature into its own file, you give your bot a clean, readable and scalable architecture. In this guide we build a modular bot skeleton from scratch with discord.py 2.x.

What is a cog and why use one?

A cog is a Python class that inherits from commands.Cog and holds commands and listeners inside it. While the bot core keeps running, you can load, unload or reload cogs at runtime. The practical benefits are:

  • Separation of concerns: Each domain — moderation, music, economy — lives in its own file.
  • Hot reloading: Reload a single module without shutting the bot down to test changes.
  • Team work: Different developers work on different cogs without conflicts.
  • Less global state: Each cog keeps its own state inside the class.

Project structure

The recommended layout keeps the core at the root and puts all cogs in a dedicated folder:

my-bot/
├── bot.py
├── cogs/
│   ├── moderation.py
│   ├── general.py
│   └── economy.py
├── requirements.txt
└── .env

Use a virtual environment to install dependencies, and add at least discord.py and python-dotenv to your requirements.txt:

python -m venv .venv
source .venv/bin/activate
pip install -U discord.py python-dotenv

The core file: bot.py

In discord.py 2.x the right place to load extensions is the setup_hook method. It is called before the bot logs in, and load_extension can be safely awaited there. We subclass commands.Bot to set up a clean entry point:

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

load_dotenv()

INITIAL_EXTENSIONS = [
    "cogs.general",
    "cogs.moderation",
    "cogs.economy",
]

class MyBot(commands.Bot):
    def __init__(self):
        intents = discord.Intents.default()
        intents.message_content = True
        super().__init__(command_prefix="!", intents=intents)

    async def setup_hook(self):
        for ext in INITIAL_EXTENSIONS:
            await self.load_extension(ext)
        # Sync slash commands
        await self.tree.sync()

    async def on_ready(self):
        print(f"Logged in as {self.user} (ID: {self.user.id})")

async def main():
    bot = MyBot()
    await bot.start(os.environ["DISCORD_TOKEN"])

if __name__ == "__main__":
    asyncio.run(main())

You also need to enable the message_content intent in the Discord Developer Portal; otherwise prefix commands will not work.

Writing your first cog

Every cog file has two parts: a class that inherits from commands.Cog, and an asynchronous setup function at the bottom of the file. In discord.py 2.x, setup is now async and adds the cog with await bot.add_cog():

import discord
from discord.ext import commands

class General(commands.Cog):
    def __init__(self, bot: commands.Bot):
        self.bot = bot

    @commands.command(name="ping")
    async def ping(self, ctx: commands.Context):
        latency = round(self.bot.latency * 1000)
        await ctx.send(f"Pong! {latency}ms")

    @commands.Cog.listener()
    async def on_member_join(self, member: discord.Member):
        channel = member.guild.system_channel
        if channel is not None:
            await channel.send(f"Welcome {member.mention}!")

async def setup(bot: commands.Bot):
    await bot.add_cog(General(bot))

Two things to watch: inside a cog, the first parameter of any command is always self, and listeners are defined with the @commands.Cog.listener() decorator — never @bot.event.

Moving slash commands into cogs

Modern bots increasingly lean on slash commands. You define them inside a cog with app_commands:

from discord import app_commands
from discord.ext import commands
import discord

class Economy(commands.Cog):
    def __init__(self, bot: commands.Bot):
        self.bot = bot

    @app_commands.command(name="balance", description="Show your balance")
    async def balance(self, interaction: discord.Interaction):
        await interaction.response.send_message(
            "Your balance: 1000 coins", ephemeral=True
        )

async def setup(bot: commands.Bot):
    await bot.add_cog(Economy(bot))

Slash commands must be synced to be registered with Discord. During development, syncing to a specific test guild (which shows up instantly) is far faster than a global sync; global propagation can take up to an hour.

Managing cogs at runtime

By adding an admin cog you can load, unload and reload modules without restarting the bot. This dramatically speeds up the development loop:

class Admin(commands.Cog):
    def __init__(self, bot: commands.Bot):
        self.bot = bot

    @commands.command()
    @commands.is_owner()
    async def reload(self, ctx, extension: str):
        await self.bot.reload_extension(f"cogs.{extension}")
        await ctx.send(f"`{extension}` reloaded.")

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

The @commands.is_owner() check restricts these sensitive commands to the bot owner only. If a module has a syntax error, reload_extension keeps the old version and raises the error — so catching the exception and reporting it back to the user is a good habit.

Frequently Asked Questions

Why does the setup function have to be async?

With discord.py 2.x the library's lifecycle became fully asynchronous. Since add_cog and load_extension are now coroutines, the setup function that calls them must also be async def and use await inside.

Should I use prefix commands or slash commands?

For new projects, slash commands are recommended: they are discoverable in the Discord UI, auto-complete, and do not require the message_content intent. The cog structure happily hosts both inside the same class.

If one cog breaks, does the whole bot crash?

No. If a cog fails to load during load_extension, only that extension is skipped; wrap it in try/except and the other cogs keep loading fine. At runtime, each cog has its own error-handling scope.

Want to migrate your bot to a clean cog architecture? Whether you need to move an existing single-file bot to a modular structure, add slash commands, or build a Discord bot from scratch, get in touch with me.

Bu kategorideki tüm yazılar →

Devamı için