Skip to content
CreateDiscordBot
Menu

discord.js vs discord.py: which should you use for a Discord bot?

JavaScript or Python for a Discord bot? discord.js and discord.py compared in 2026: releases, maintenance, API coverage, typing, hosting, and one bot in both.

By Adam Peleback. Updated . 12 min read.

On this page
  1. JavaScript vs Python for Discord bots at a glance
  2. How active is each library in 2026?
  3. API coverage: can both do everything?
  4. The same bot in both
  5. Typing and editor support
  6. Structure and ecosystem
  7. Performance: does the language matter?
  8. Hosting
  9. What about py-cord, nextcord and disnake?
  10. How to choose
  11. How I tested the two bots

Use the language you already know. In September 2026, discord.js 14.27 and discord.py 2.7 cover the same Discord features: slash commands, Components v2 layouts, polls, modals with checkboxes, and the end-to-end encrypted voice Discord has required since March 1, 2026. Neither can build a bot the other can't, so JavaScript vs Python for a Discord bot comes down to your skills, your other code and how the libraries feel day to day.

If you know neither, the practical differences are these. discord.js ships more often (15 stable releases in the last 12 months against 3), bundles TypeScript types, and shares a language with a web dashboard. discord.py needs less code per command, because a Python function signature declares the command's options, and it has task loops and prefix-plus-slash "hybrid" commands built in.

A disclosure: I build client bots in discord.js, and every case study on my work page runs on discord.js 14. I also fix and upgrade discord.py bots. Everything below is checked against the libraries' own releases, repositories and docs on September 27, 2026, and both example bots were tested the same day.

JavaScript vs Python for Discord bots at a glance

discord.js discord.py
Language JavaScript or TypeScript on Node.js Python
Latest stable release 14.27.0, July 15, 2026 2.7.1, March 3, 2026
Stable releases in the last 12 months 15 3
Runtime it requires Node.js 18 or newer (use 22 or 24) Python 3.8 or newer
License Apache-2.0 MIT
GitHub stars (September 2026) about 26,800 about 16,200
How a command's options are defined A builder chain, separate from the handler The handler's parameters and type hints
How commands are registered Your own deploy script, or commands.set() tree.sync()
Type checking Bundled TypeScript definitions Inline type hints, checked with Pyright or mypy
Extras in the box Builders, Collection, a sharding manager Prefix and hybrid commands, task loops, cogs, UI view classes, an auto-sharded client
Voice Separate @discordjs/voice package discord.py[voice] extra
An uncaught exception inside a command Re-emitted as the client's error event; with no listener for it, the process exits Logged by the command tree; the bot keeps running

That last row is the one that bites beginners. I checked both behaviors while testing the bots below: a discord.js bot with no error listener exits the first time a command throws outside a try/catch, while discord.py logs the traceback and carries on.

How active is each library in 2026?

Both are around 11 years old and both are maintained. They differ in rhythm.

discord.js shipped 15 v14 releases between September 2025 and September 2026, in bursts rather than on a schedule, most recently 14.27.0 in July. Version 15 is being built on the main branch and published to npm as development builds, the newest on September 22, 2026; the stable line is still v14. The main branch had 250 commits from 49 contributors in those 12 months, and the v14 branch another 154.

discord.py releases a few times a year: 2.6.4 in October 2025, then 2.7.0 and 2.7.1 in February and March 2026. Work continues between releases. The master branch had 110 commits from 26 contributors in the same 12 months, 40 of them by its creator, Danny (Rapptz). The catch of that rhythm is that fixes wait. A fix for a deprecation warning that 2.7.1 raises under Python 3.14 (hidden by default) was merged in April 2026 and is still unreleased in late September.

The history matters because people still ask whether discord.py is dead. On August 28, 2021, Danny announced he was stepping down and put the repository in read-only mode, largely over Discord's privileged intents and the message content changes. On March 6, 2022, he resumed development, because Discord was about to shut down the API version the stable release used. The forks you still see, py-cord, nextcord and disnake, all started in that gap. More on them in py-cord, nextcord and disnake compared.

API coverage: can both do everything?

Yes, with a few months' difference in when new Discord features land. These are the recent additions that matter most, with the first version that supported them:

Discord feature discord.js discord.py
Polls 14.15.0, May 2024 2.4.0, June 2024
Components v2 (containers, sections, text displays) 14.19.0, April 2025 2.6.0, August 2025
End-to-end encrypted voice (DAVE) @discordjs/voice 0.19.0, August 2025 2.7.0, February 2026
Checkbox and radio inputs in modals 14.26.0, March 2026 2.7.0, February 2026
User-installable apps 14.16.0, September 2024 2.4.0, June 2024

Voice deserves a note. Discord has only allowed end-to-end encrypted voice connections since March 1, 2026, so a voice bot on a library version without DAVE support can no longer join calls. discord.py added it in 2.7.0 on February 27, 2026, two days before the deadline. discord.js had it in its voice package since August 2025, and since @discordjs/voice 0.19.1 the DAVE library is always installed. If you inherit an old music or voice bot, the library version is the first thing to check.

Both libraries need extra pieces for audio. In discord.js that is @discordjs/voice plus an Opus encoder; in discord.py it is pip install -U "discord.py[voice]". Both use FFmpeg to play common audio files such as MP3.

The same bot in both

Here is one bot written twice: a /roll command with two optional whole-number options, registered in one test server, with a private error message if something fails.

The Discord side is identical for both: create an application, copy its token, and invite the bot to a test server. Steps 1 to 4 of my discord.js tutorial walk through it, and none of it depends on the language. Both versions then read the same .env file, next to the code. GUILD_ID is your test server's ID, which you copy by right-clicking the server with Developer Mode on:

.env
DISCORD_TOKEN=paste-your-bot-token-here
GUILD_ID=your-test-server-id

discord.js

This version loads .env with Node's built-in --env-file flag. Run it on Node.js 22 or 24, the release lines that still get security fixes.

package.json
{
  "name": "roll-bot",
  "private": true,
  "type": "commonjs",
  "scripts": {
    "start": "node --env-file=.env bot.js"
  },
  "dependencies": {
    "discord.js": "^14.27.0"
  }
}
bot.js
const { Client, Events, GatewayIntentBits, MessageFlags, SlashCommandBuilder } = require('discord.js');

const roll = new SlashCommandBuilder()
  .setName('roll')
  .setDescription('Roll some dice')
  .addIntegerOption((option) =>
    option.setName('dice').setDescription('How many dice (default 1)').setMinValue(1).setMaxValue(10),
  )
  .addIntegerOption((option) =>
    option.setName('sides').setDescription('Sides per die (default 6)').setMinValue(2).setMaxValue(100),
  );

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

client.once(Events.ClientReady, async (readyClient) => {
  // Register /roll in one test server. Guild commands update instantly.
  const guild = await readyClient.guilds.fetch(process.env.GUILD_ID);
  await guild.commands.set([roll]);
  console.log(`Logged in as ${readyClient.user.tag}`);
});

client.on(Events.InteractionCreate, async (interaction) => {
  if (!interaction.isChatInputCommand() || interaction.commandName !== 'roll') return;

  try {
    const dice = interaction.options.getInteger('dice') ?? 1;
    const sides = interaction.options.getInteger('sides') ?? 6;
    const rolls = Array.from({ length: dice }, () => 1 + Math.floor(Math.random() * sides));
    const total = rolls.reduce((sum, value) => sum + value, 0);
    await interaction.reply(`You rolled ${rolls.join(', ')} (total ${total}).`);
  } catch (error) {
    console.error(error);
    if (!interaction.replied) {
      await interaction.reply({ content: 'Something went wrong.', flags: MessageFlags.Ephemeral }).catch(console.error);
    }
  }
});

// discord.js re-emits errors from async listeners here; without this listener they crash the process.
client.on(Events.Error, console.error);

client.login(process.env.DISCORD_TOKEN);
terminal
npm install
npm start

discord.py

This version needs Python 3.10 or newer, because python-dotenv does. I tested it on 3.12 and 3.14.

requirements.txt
discord.py>=2.7,<3
python-dotenv>=1.2
bot.py
import logging
import os
import random

import discord
from discord import app_commands
from dotenv import load_dotenv

load_dotenv()
GUILD = discord.Object(id=int(os.environ["GUILD_ID"]))
log = logging.getLogger("roll-bot")


class Bot(discord.Client):
    def __init__(self):
        # default() turns on every intent that isn't privileged.
        super().__init__(intents=discord.Intents.default())
        self.tree = app_commands.CommandTree(self)

    async def setup_hook(self):
        # Register the commands in one test server. Guild commands update instantly.
        self.tree.copy_global_to(guild=GUILD)
        await self.tree.sync(guild=GUILD)


bot = Bot()


@bot.event
async def on_ready():
    log.info("Logged in as %s", bot.user)


@bot.tree.command(description="Roll some dice")
@app_commands.describe(dice="How many dice (default 1)", sides="Sides per die (default 6)")
async def roll(
    interaction: discord.Interaction,
    dice: app_commands.Range[int, 1, 10] = 1,
    sides: app_commands.Range[int, 2, 100] = 6,
):
    rolls = [random.randint(1, sides) for _ in range(dice)]
    await interaction.response.send_message(f"You rolled {', '.join(map(str, rolls))} (total {sum(rolls)}).")


@bot.tree.error
async def on_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
    log.error("Command failed", exc_info=error)
    if not interaction.response.is_done():
        await interaction.response.send_message("Something went wrong.", ephemeral=True)


# root_logger=True sends our log lines through the same handler discord.py sets up.
bot.run(os.environ["DISCORD_TOKEN"], root_logger=True)
terminal
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python bot.py

On Windows, activate the environment with .venv\Scripts\activate instead.

What the two versions show

  • Options. discord.js describes each option in a builder chain, and the handler reads them back by name. discord.py reads the function signature: dice: app_commands.Range[int, 1, 10] = 1 is the option's type, limits and default in one place. That is why each command takes fewer lines in Python, even though this Python file is longer overall because of its class and logging setup.
  • Registration. Both register the command in one server on startup, because guild commands appear instantly (how slash command registration works). discord.py's official examples call tree.sync() in setup_hook the same way. In discord.js, bigger bots usually move this into a separate deploy script, as the tutorial does.
  • Intents. The discord.js bot lists exactly one intent, Guilds. discord.py's Intents.default() turns on every non-privileged intent, including message and typing events this bot never uses. Both are fine; discord.js makes you choose, discord.py chooses for you.
  • Errors. The discord.js version wraps the handler in try/catch and still adds an error listener, because without it an uncaught error ends the process. discord.py routes command errors to @bot.tree.error, and without that handler it logs them for you.
  • Secrets. Node reads .env itself with --env-file. Python needs the python-dotenv package, or the variables set by your shell or host.

For a longer Python walkthrough with cogs, buttons and one error handler, see how to make a Discord bot in Python.

Typing and editor support

discord.js ships TypeScript definitions for both require and import. Autocomplete works in plain JavaScript in VS Code, and if you write TypeScript, checks like interaction.isChatInputCommand() narrow the type, so the editor knows which methods exist inside the if.

discord.py is type-hinted and ships the marker file that tells type checkers so, which means Pyright, mypy and VS Code's Pylance read its types directly. The type hints do double duty in slash commands, since they define the options Discord shows.

If static typing matters to you, both are solid. TypeScript's editor tooling is more widespread, and a TypeScript bot can share types with a TypeScript web dashboard.

Structure and ecosystem

discord.py includes more. discord.ext.commands gives you prefix commands and hybrid commands that work as both !command and /command (the prefix side needs the Message Content intent). discord.ext.tasks runs loops, for example checking an API every five minutes. Cogs group commands and listeners into classes you can reload, and discord.ui.View classes put a button's handler next to the button.

discord.js leaves more to you or to add-ons. You write the command handler yourself, which the official guide walks through, or use a framework such as Sapphire. Buttons are routed by their custom ID in your interactionCreate listener or through collectors. Scheduled jobs are plain setInterval or a cron package.

JavaScript's pull is the web. Discord logins, dashboards, webhooks and serverless functions are everyday JavaScript. On Rivals League, the Discord provisioning scripts, the Twitch go-live notifier and the Discord login all live in the same JavaScript and TypeScript repository as the league's website, and share its Supabase database. Discord's own getting-started tutorial is written in JavaScript too.

Python's pull is data. If the bot's real work is analysis, machine learning, pulling data from your own sources or scientific libraries, writing the bot in Python keeps it all in one language.

Performance: does the language matter?

Rarely. A Discord bot spends its time waiting for the network, and both libraries handle that the same way: one event loop, with Node's async model in discord.js and asyncio in discord.py. What limits a bot is Discord itself:

  • 50 requests per second across the HTTP API for the whole bot, plus per-route limits. Both libraries queue requests and wait out rate limits for you.
  • 120 gateway events per 60 seconds that a bot can send on one connection.
  • Sharding is required from 2,500 servers. discord.js offers a sharding manager that runs shards as separate processes or threads, or internal sharding in one process. discord.py has AutoShardedClient, which runs all shards in one process and is recommended from about 1,000 servers.

Memory depends on what the library caches far more than on the language. discord.js keeps up to 200 messages per channel by default and lets you tune every cache and sweep old entries. discord.py keeps 1,000 messages in total by default, and you can limit member caching and skip loading every member at startup.

Where language speed does show is CPU-heavy work inside the bot, such as image generation or crunching large files. That blocks the event loop in both, and the fix is the same: move it to a worker thread or process.

Hosting

A discord.js or discord.py bot is a long-running process that keeps a connection to Discord open, so it needs somewhere that stays on: a VPS, a container platform or a bot host. Both languages run on the same hosts. PebbleHost, for example, lists one $3.00-a-month plan with 1 GB of memory for Java, Python and Node.js bots (as of September 2026).

Serverless platforms that only run code per request are a different story. They can't hold a gateway connection, so a bot there has to receive slash commands over HTTP through an Interactions Endpoint URL instead, and both libraries are built around the gateway. That setup works in either language, but it is a different architecture from the bots on this page.

My guide to hosting a Discord bot 24/7 goes through free and paid options for both.

What about py-cord, nextcord and disnake?

These three forks started from discord.py's code in August 2021, while discord.py was paused. All three are still maintained:

Fork Import name Latest release Stable releases in the last 12 months Python GitHub stars (September 2026)
py-cord discord 2.8.1, July 25, 2026 5, plus 3 release candidates 3.10 to 3.14 2,957
nextcord nextcord 3.2.0, May 21, 2026 1 3.12 or newer 1,260
disnake disnake 2.12.1, July 22, 2026 2 3.10 or newer 772

Each has caught up with the recent Discord features in its own time. DAVE voice support arrived in disnake 2.12.0 (February 2026), py-cord 2.8.0 (May 2026) and nextcord 3.2.0 (May 2026). Components v2 arrived in disnake 2.11.0 (September 2025), was overhauled in py-cord 2.7.0 (December 2025), and arrived as initial, lower-level support in nextcord 3.2.0.

Watch the import names. py-cord installs as discord, the same package name as discord.py, so the two can't live in the same environment, and a tutorial written for one can fail on the other in confusing ways. Check which one pip list shows before debugging anything else.

For a new Python bot in 2026, I'd pick discord.py: its community is by far the largest of the four (about 16,200 GitHub stars against 2,957 for py-cord, the biggest fork), and the reason the forks were created, discord.py's shutdown, no longer applies. Stay on a fork if your bot already uses it and works. Each fork added its own slash command API, so moving to discord.py means rewriting that part of the code.

How to choose

Pick discord.js if:

  • you know JavaScript or TypeScript, or the bot sits next to a website, dashboard or API written in it
  • you want static types with the most common tooling
  • you value frequent releases that follow Discord's changes closely

Pick discord.py if:

  • you know Python, or the bot's real work uses Python libraries
  • you want the shortest command code, with options declared in the function signature
  • you'd use its built-in task loops, cogs and hybrid commands

If the bot is for a business, a league or a paid community, the library matters less than who maintains the code after launch. Whichever you choose, keep it on a current version: Discord changes things every year, and the voice encryption deadline in March 2026 cut off voice bots running library versions without DAVE.

How I tested the two bots

I can't log in to Discord from a test script without a real token, so each file ran unchanged against a fake Discord connection: login and every HTTP request were replaced by recorders, and interaction payloads shaped like Discord's went through each library's own interaction handling.

Check discord.js 14.27.0, Node.js 24.15.0 discord.py 2.7.1, Python 3.12.13 and 3.14.3
Startup Token read from .env; only the Guilds intent Token and server ID read from .env by python-dotenv 1.2.3; default intents, none privileged
Registration PUT to the server's command route with /roll, options 1 to 10 and 2 to 100, both optional Same route and the same option limits from tree.sync()
/roll with defaults One die from 1 to 6 One die from 1 to 6
/roll dice:3 sides:20 Three values from 1 to 20 and the right total Three values from 1 to 20 and the right total
A command that raises an error Error logged, private "Something went wrong." Error logged through @bot.tree.error, private "Something went wrong."
Warnings None None from the bot's code. The library itself raises one hidden DeprecationWarning on each version: about audioop on 3.12, and about asyncio.iscoroutinefunction on 3.14 (the one fixed on master but not yet released)

I also installed requirements.txt into a fresh virtual environment to check that it resolves to discord.py 2.7.1 and python-dotenv 1.2.3.

If you'd rather not maintain a bot in either language, I build custom Discord bots on your own application and hand over the code, usually in discord.js. If you already have a discord.js or discord.py bot that broke after an update or stopped joining voice, Bot Rescue gets it running again for a fixed $149, and you pay nothing if I can't fix it.

Questions

Is JS or Python better for Discord bots?

Neither is better at the Discord part. discord.js and discord.py both cover slash commands, Components v2, polls, modals and encrypted voice as of September 2026. Use the language you already know; if you know neither, discord.js has more frequent releases and bundled TypeScript types, while discord.py needs less code per command.

Is discord.py still maintained?

Yes. Its creator paused development in August 2021 and resumed it in March 2022, and the latest release is 2.7.1 from March 3, 2026. The repository had 110 commits from 26 contributors in the 12 months to September 2026, and releases come a few times a year rather than monthly.

Do discord.py voice bots still work after Discord's encryption change?

Yes, from discord.py 2.7.0. Discord has only allowed end-to-end encrypted (DAVE) voice connections since March 1, 2026, and discord.py added DAVE support in 2.7.0 on February 27, 2026. Install it with pip install -U "discord.py[voice]", which pulls in the davey package it needs.

Can I switch from discord.py to discord.js later, or the other way?

Yes. Both talk to the same Discord API, so commands, permissions, intents, your application and its token stay the same, and data in a database carries over. You rewrite the code, not the design, and the effort grows with the number of commands and integrations.

Can I use TypeScript with discord.js?

Yes. discord.js ships its own type definitions for both require and import, so TypeScript works without extra packages, and type narrowing works on methods like isChatInputCommand(). You compile to JavaScript with tsc or run it through a TypeScript-aware runner.

Sources

Prices and features were checked on September 27, 2026.

  1. discord.js on npm (versions and release dates, via the npm registry)
  2. discord.js releases on GitHub (polls 14.15.0, user-installable apps 14.16.0, Components v2 14.19.0, checkbox and radio 14.26.0, voice DAVE 0.19.0)
  3. discord.js repository (stars, license, v15 development on main)
  4. @discordjs/voice on npm (0.19.2, DAVE via @snazzah/davey)
  5. discord.js Guide, App sharding (2,500-guild requirement, internal sharding)
  6. discord.py on PyPI (versions, release dates, Python requirement)
  7. discord.py changelog (polls and user-installable apps 2.4.0, Components v2 2.6.0, DAVE and modal checkboxes 2.7.0)
  8. discord.py repository (stars, license, commit history)
  9. Rapptz, The Future of discord.py (August 28, 2021)
  10. Rapptz, On Resuming discord.py Development (March 6, 2022)
  11. py-cord on PyPI (2.8.1, Python 3.10 to 3.14)
  12. py-cord changelog (Components V2 overhaul in 2.7.0, DAVE in 2.8.0)
  13. nextcord on PyPI (3.2.0, Python 3.12 or newer)
  14. nextcord changelog (Components V2 and DAVE in 3.2.0)
  15. disnake on PyPI (2.12.1, Python 3.10 or newer)
  16. disnake changelog (Components v2 in 2.11.0, DAVE in 2.12.0)
  17. Discord Developer Docs, Voice connections (E2EE required from March 1, 2026)
  18. Discord Developer Docs, Rate limits (50 requests per second)
  19. Discord Developer Docs, Gateway (120 events per 60 seconds, sharding)
  20. Discord Developer Docs, Building your first Discord app (JavaScript example app)
  21. python-dotenv on PyPI (1.2.3)
  22. PebbleHost, Discord bot hosting (Java, Python and Node.js at $3.00 a month)

Tell me what your server needs

Send a short brief. You get a reply within one business day with questions or a price range, and a fixed quote before any work starts.