Async-first Discord API wrapper for Python 3.11+
Fully typed, modern architecture built for performance, correctness, and developer experience.
- Async-first design built for Python 3.11+
- Immutable, slotted dataclass models
- Fully typed API with strict mypy support (PEP 561)
- Typed event system no raw dict payloads
- High-performance JSON via orjson
- Pluggable caching (NoCache, TTLCache, or custom strategies)
- Built-in slash command system with autocomplete, components, and modals
- Structured error system with 20+ exception types
- Flexible sharding (auto, manual, distributed)
- Event middleware (before/after hooks)
discordium is slash-first. The slash command system is fully featured: autocomplete, subcommands, components, modals, typed options, and permission guards.
Prefix commands (CommandRouter) are supported as a lightweight secondary
option useful for quick development bots or mixed environments. They are intentionally
minimal and not on par with the slash system.
pip install discordium
pip install discordium[speed] # aiodns + Brotli
pip install discordium[voice] # PyNaClimport discordium
from discordium.ext.slash import SlashRouter
from discordium.models.events import ReadyEvent
from discordium.models.interaction import Interaction
bot = discordium.GatewayClient(
token="YOUR_TOKEN",
intents=discordium.Intents.default(),
)
slash = SlashRouter()
@slash.command(name="ping", description="Check latency")
async def ping(inter: Interaction) -> None:
await inter.respond("Pong!", ephemeral=True)
@bot.on_event("ready")
async def on_ready(event: ReadyEvent) -> None:
print(f"Online as {event.user.display_name}!")
slash.attach(bot)
bot.run()Avoid magic strings with the Events constants or the EventName literal type:
from discordium.models.event_names import Events, EventName
@bot.on_event(Events.MESSAGE_CREATE)
async def on_msg(event): ...
@bot.on_event(Events.GUILD_MEMBER_ADD)
async def on_join(event): ...Every event handler receives a proper typed object no more data: dict:
from discordium.models.events import MessageCreateEvent
@bot.on_event("message_create")
async def on_msg(event: MessageCreateEvent) -> None:
if event.message.content == "hello":
await event.message.reply("Hey!")30+ typed events supported (messages, guilds, members, reactions, threads, voice, etc.)
@slash.command(name="info")
async def info(inter: Interaction) -> None:
user = inter.option_user("user")
detailed = inter.option_bool("detailed", default=False)from discordium.models.components import *
row = ActionRow(
Button(label="Yes", custom_id="vote:yes"),
Button(label="No", custom_id="vote:no"),
)
await inter.respond("Vote:", components=[row])
@slash.on_component("vote:")
async def on_vote(inter):
choice = inter.custom_id.split(":")[1]
await inter.respond(f"You voted {choice}!", ephemeral=True)modal = Modal(title="Feedback", custom_id="feedback")
modal.add_field(label="Message", custom_id="msg")
await inter.send_modal(modal)
@slash.on_modal("feedback")
async def on_fb(inter):
fields = inter.get_all_fields()
await inter.respond(fields["msg"])try:
await rest.ban_member(guild_id, user_id)
except discordium.Forbidden:
...
except discordium.NotFound:
...@bot.before_event
async def before(event_name, event):
...
@bot.after_event
async def after(event_name, event):
...from discordium.ext.guards import has_permissions, cooldown
@slash.command(name="ban")
@has_permissions(...)
@cooldown(rate=1, per=30)
async def ban(inter):
...from discordium.ext.tasks import loop
@loop(minutes=5)
async def task():
...
task.start()file = discordium.File(data.encode(), filename="export.txt")
await rest.send_message(channel_id, "Here:", files=[file])wh = await rest.create_webhook(channel_id, name="Alerts")
await wh.send("Server alert!")discordium/
├── client.py
├── errors.py
├── models/
├── gateway/
├── http/
├── cache/
├── ext/
└── utils/
Discordium follows Semantic Versioning.
During the 0.x phase, breaking changes may occur between releases.
MIT