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
| Error | Fix |
|---|---|
ExtensionNotFound | The 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. |
NoEntryPointError | The file has no setup function. Add async def setup(bot). |
RuntimeWarning: coroutine ... was never awaited | You called load_extension or add_cog without await. This is 1.x-style code. |
ExtensionAlreadyLoaded | You're loading the same extension twice, for example in on_ready. Load once in setup_hook. |
ExtensionFailed | The 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.