Skip to content

Migrating from discord.py to ScurryPy


Note

This guide shows the philosophical difference between the two libraries. For in-depth ScurryPy patterns, see the Examples page

Both discord.py and ScurryPy can build the same bots. The difference is in how they approach complexity.

discord.py hides it behind framework abstractions. ScurryPy shows it explicitly in your code.

Below is discord.py's "basic bot" example implemented in both libraries. Notice they're roughly the same length, but one uses framework magic, while the other uses plain Python.

# This example requires the 'members' and 'message_content' privileged intents to function.

import discord
from discord.ext import commands
import random

description = """An example bot to showcase the discord.ext.commands extension
module.

There are a number of utility commands being showcased here."""

intents = discord.Intents.default()
intents.members = True
intents.message_content = True

bot = commands.Bot(command_prefix='?', description=description, intents=intents)


@bot.event
async def on_ready():
    # Tell the type checker that User is filled up at this point
    assert bot.user is not None

    print(f'Logged in as {bot.user} (ID: {bot.user.id})')
    print('------')


@bot.command()
async def add(ctx, left: int, right: int):
    """Adds two numbers together."""
    await ctx.send(left + right)


@bot.command()
async def roll(ctx, dice: str):
    """Rolls a dice in NdN format."""
    try:
        rolls, limit = map(int, dice.split('d'))
    except Exception:
        await ctx.send('Format has to be in NdN!')
        return

    result = ', '.join(str(random.randint(1, limit)) for r in range(rolls))
    await ctx.send(result)


@bot.command(description='For when you wanna settle the score some other way')
async def choose(ctx, *choices: str):
    """Chooses between multiple choices."""
    await ctx.send(random.choice(choices))


@bot.command()
async def repeat(ctx, times: int, content='repeating...'):
    """Repeats a message multiple times."""
    for i in range(times):
        await ctx.send(content)


@bot.command()
async def joined(ctx, member: discord.Member):
    """Says when a member joined."""
    # Joined at can be None in very bizarre cases so just handle that as well
    if member.joined_at is None:
        await ctx.send(f'{member} has no join date.')
    else:
        await ctx.send(f'{member} joined {discord.utils.format_dt(member.joined_at)}')


bot.run('token')
# --- Environment setup ---
import os
from dotenv import load_dotenv
load_dotenv()

TOKEN = os.getenv("BOT_TOKEN")
APP_ID = 0
GUILD_ID = 0

# --- Core library imports ---
from scurrypy import Client, Intents, EventTypes, ReadyEvent, MessageCreateEvent, Channel, GuildMemberAddEvent

intents = Intents.set(message_content=True, guild_members=True)

client = Client(token=TOKEN, intents=intents)

import random

async def on_ready(event: ReadyEvent):
    bot_user = event.user
    print(f"Logged in as {bot_user.username} (ID: {bot_user.id})")

client.add_event_listener(EventTypes.READY, on_ready)

async def add(channel: Channel, a: int, b: int):
    """Adds two numbers together."""
    await channel.send(f"{a} + {b} = **{a + b}**")

async def roll(channel: Channel, dice: str):
    """Rolls a dice in NdN format."""
    try:
        rolls, limit = map(int, dice.split('d'))
    except:
        await channel.send('Format has to be in NdN!')
        return

    result = ', '.join(str(random.randint(1, limit)) for r in range(rolls))

    await channel.send(result)

async def choose(channel: Channel, choices):
    """Chooses between multiple choices."""
    await channel.send(f"{random.choice(choices)}")

async def repeat(channel: Channel, times: int):
    """Repeats a message multiple times."""
    for _ in range(times):
        await channel.send("Repeating...")

WELCOME_CHANNEL_ID = 0

async def joined(event: GuildMemberAddEvent):
    member = event.user
    channel = client.channel(WELCOME_CHANNEL_ID) # create a channel resource to send the message

    if event.joined_at is None:
        await channel.send(f"{member.username} has no join date.")
    else:
        await channel.send(f"{member.username} joined {event.joined_at}")

client.add_event_listener(EventTypes.GUILD_MEMBER_ADD, joined)

async def dispatch(event: MessageCreateEvent):
    """Manual command dispatcher (equivalent to discord.py commands.Bot)."""
    if not event.content:
        return # ignore event for this callback if no content is present (likely missing intent)

    if event.author.id == APP_ID:
        return # ignore bot messages

    if not event.content.startswith('!'):
        return # ignore non-command messages

    command, *args = event.content.split(' ')

    channel = client.channel(event.channel_id, context=event)

    try:
        match command:
            case '!roll':
                await roll(channel, args[0])
            case '!choose':
                await choose(channel, args)
            case '!repeat':
                await repeat(channel, int(args[0]))
            case '!add':
                await add(channel, int(args[0]), int(args[1]))
            case _:
                await channel.send(f"Command '{command}' not recognized!")
    except IndexError:
        await channel.send("Invalid command arguments!")

client.add_event_listener(EventTypes.MESSAGE_CREATE, dispatch)

client.run()

Key Differences


Action discord.py ScurryPy
Command Registration Magic registry Explicit routing
Argument Parsing Through magic ctx Explicit parsing
Context vs Event What is ctx? msg is user-created
Error Handling ??? Explicit error catching + handling

Which Should You Use?


Use discord.py if:

  • You want to get started fast
  • You trust framework magic
  • You're building simple bots
  • You don't need to understand internals
  • You're fine with debugging framework behavior

Use ScurryPy if:

  • You want to understand what's happening
  • You need custom routing/parsing
  • You're building frameworks or complex systems
  • You value explicit over convenient
  • You want full control of your architecture

The Trade-off


discord.py optimizes for ease of use.

Less code to write initially, but harder to customize or debug.

ScurryPy optimizes for understanding.

Same amount of code, but you know exactly what each line does.

Both approaches are valid. Choose based on your priorities.

Next Steps...


Check out the Examples to see ScurryPy in action, or dive into Getting Started to build your first bot.

Want the best of both worlds? Try ScurryKit, a framework built on ScurryPy that provides decorator-style convenience while keeping the architecture clean.