NewSchokr 13 is here. A whole new UI, seven locations, snapshots and more. Read more →

How to organise a discord.py bot with cogs

Split a discord.py 2.x bot into cogs and extensions, load them in setup_hook, and fix common errors like ExtensionNotFound and missing setup functions.

Updated · 1 min read

Cogs let you split a discord.py bot into separate files, each holding related commands, listeners and tasks. You put each cog in its own module (an "extension") inside a cogs folder and load them in setup_hook. In discord.py 2.x, loading is asynchronous, which trips up a lot of older tutorials.

Project layout

my-bot/
  bot.py
  requirements.txt
  .env
  cogs/
    __init__.py
    fun.py
    moderation.py

__init__.py can be empty.

A cog file

# cogs/fun.py
import random

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


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

    @commands.command()
    async def coin(self, ctx: commands.Context):
        await ctx.send(random.choice(["Heads", "Tails"]))

    @app_commands.command(name="roll", description="Roll a die")
    async def roll(self, interaction: discord.Interaction, sides: int = 6):
        await interaction.response.send_message(f"You rolled {random.randint(1, sides)}")

    @commands.Cog.listener()
    async def on_member_join(self, member: discord.Member):
        print(f"{member} joined {member.guild}", flush=True)


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

Every extension needs an async setup function that adds the cog. Note await bot.add_cog(...): in 2.x it's a coroutine. The on_member_join listener only fires with the members intent enabled; see privileged intents.

Loading cogs in bot.py

import os
from pathlib import Path

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

load_dotenv()

COGS_DIR = Path(__file__).resolve().parent / "cogs"


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 file in COGS_DIR.glob("*.py"):
            if file.stem != "__init__":
                await self.load_extension(f"cogs.{file.stem}")
                print(f"Loaded cogs.{file.stem}", flush=True)
        await self.tree.sync()


bot = MyBot()
bot.run(os.environ["DISCORD_TOKEN"])

setup_hook runs once before the bot connects, which is the right place to load extensions and sync slash commands. See discord.py slash commands for guild vs global sync.

Reloading a cog without restarting

@bot.command()
@commands.is_owner()
async def reload(ctx: commands.Context, name: str):
    await bot.reload_extension(f"cogs.{name}")
    await ctx.send(f"Reloaded cogs.{name}")

Upload the changed file in the file manager, then run !reload fun. If you changed slash command definitions, sync again afterwards. See updating your bot's code.

Common errors

ErrorFix
ExtensionNotFoundThe dotted path is wrong or the file wasn't uploaded. cogs/fun.py loads as "cogs.fun", and names are case-sensitive on the server.
NoEntryPointErrorThe file has no setup function. Add async def setup(bot).
RuntimeWarning: coroutine ... was never awaitedYou called load_extension or add_cog without await. This is 1.x-style code.
ExtensionAlreadyLoadedYou're loading the same extension twice, for example in on_ready. Load once in setup_hook.
ExtensionFailedThe cog raised an error while importing. Read the original error underneath it in the console.

Deploying

Upload bot.py, the cogs folder and requirements.txt, then set bot.py as the main file in your server's Startup settings. See deploy a discord.py bot to Schokr.