Guide débutant7 octobre 202616 min de lecture

Créer un bot Discord en Python : le guide complet discord.py

De zéro à un bot qui répond à des slash commands sur votre serveur, organisé en cogs, et qui reste en ligne quand vous fermez le PC. Code complet, et un tableau des erreurs qu'on voit vraiment passer.

Par Thomas Durand, Fastheberg

Python est le langage que beaucoup apprennent en premier, et discord.py est la bibliothèque qui va avec. Ce guide suit le même chemin que la version discord.js : création de l'application, intents, slash commands, événements, puis hébergement. Il s'arrête là où un bot commence à être utile, pas au premier « Pong ».

On utilise discord.py 2 et ses app_commands, la façon officielle de faire des slash commands. Tout le code est fourni, en trois fichiers. Comptez une bonne heure en lisant tout.

1. Ce qu'est un bot Discord, techniquement

Un bot Discord est un programme qui garde une connexion WebSocket ouverte vers la « Gateway » de Discord, par laquelle Discord lui pousse des événements (une commande tapée, un membre qui arrive), et qui appelle l'API REST pour agir (répondre, envoyer un message, donner un rôle). discord.py gère les deux pour vous, et vous ne voyez que des fonctions async appelées au bon moment.

Schéma : le process du bot chez l'hébergeur, relié à Discord par une connexion WebSocket permanente et par l'API REST
Tant que la connexion tient, le bot est en ligne. Si le script s'arrête, la pastille passe au gris.

Le mot async va revenir partout. discord.py repose sur asyncio : une seule boucle traite tous les événements, et chaque appel réseau se fait avec await pour ne pas bloquer les autres. La règle pratique : jamais de time.sleep ni de requests dans un bot, mais asyncio.sleep et aiohttp.

2. Ce qu'il vous faut

  • Python 3.10 ou plus récent, 3.12 de préférence. Sur Windows, cochez « Add python.exe to PATH » à l'installation. Vérifiez avec python --version.
  • Un serveur Discord de test dont vous êtes administrateur, vide de préférence.
  • Un éditeur, VS Code avec l'extension Python par exemple.
  • Le Mode développeur activé dans Discord (Paramètres, Avancés), pour copier l'identifiant du serveur de test par clic droit.

3. Créer l'application dans le portail développeur

Direction discord.com/developers/applications. Cinq écrans, dans cet ordre, et c'est identique quel que soit le langage.

Les cinq étapes du portail développeur : New Application, onglet Bot et Reset Token, intents privilégiés, OAuth2 URL Generator, invitation
  1. 1

    New Application, un nom, et c'est tout.

  2. 2

    Onglet Bot, Reset Token. Le token s'affiche une fois. Copiez-le dans un endroit sûr : c'est le mot de passe du bot. Décochez « Public Bot » si le bot est pour vous seul.

  3. 3

    Privileged Gateway Intents. Laissez les trois interrupteurs éteints pour l'instant. On activera « Server Members Intent » à la section 10 pour le message de bienvenue.

  4. 4

    OAuth2, URL Generator. Scopes bot et applications.commands, le second étant obligatoire pour les slash commands. Permissions : Send Messages, Embed Links, Read Message History. Pas Administrator.

  5. 5

    Ouvrir l'URL et inviter le bot sur le serveur de test. Il apparaît hors ligne, c'est normal.

Notez l'identifiant de votre serveur de test (clic droit sur le serveur, Copier l'identifiant). Avec discord.py, l'identifiant de l'application n'est pas nécessaire : la bibliothèque le déduit du token.

4. Comprendre les intents

Les intents sont les familles d'événements que vous demandez à Discord. En discord.py, ils se règlent sur un objet discord.Intents. Intents.default() active tout ce qui n'est pas privilégié, et c'est exactement ce qu'il faut pour les slash commands.

Intents standard à gauche, intents privilégiés (Message Content, Server Members, Presence) à droite, avec la règle : ne demander que ce que le bot utilise

Trois intents sont privilégiés : message_content, members et presences. Pour eux, il faut l'activer dans le code et cocher la case dans le portail. L'oubli de la case produit l'exception PrivilegedIntentsRequired au démarrage, de loin l'erreur la plus fréquente dans les consoles qu'on lit au support.

5. Initialiser le projet Python

Un environnement virtuel évite de mélanger les dépendances de vos projets. Dans un dossier vide :

Terminal
python -m venv .venv
# Windows :
.venv\Scripts\activate
# macOS / Linux :
source .venv/bin/activate

pip install -U "discord.py>=2.4" python-dotenv
pip freeze > requirements.txt

Le fichier requirements.txt est ce que lira votre hébergeur pour installer les mêmes bibliothèques. Créez ensuite le .env et le .gitignore :

.env
DISCORD_TOKEN=le_token_copié
GUILD_ID=identifiant_serveur_test
.gitignore
.venv/
__pycache__/
.env
Arborescence visée à la fin du guide
mon-bot/
├── .env
├── .gitignore
├── requirements.txt
├── bot.py                # point d'entrée : connexion, chargement des cogs, synchronisation
└── cogs/
    ├── utilitaires.py    # /ping, /user
    └── fun.py            # /roll

6. Le premier bot : trois slash commands

On commence volontairement avec tout dans un seul fichier, pour voir l'ensemble d'un coup. À la section 9, on le découpera en cogs. Créez bot.py :

bot.py
import os
import random

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

load_dotenv()
TOKEN = os.environ["DISCORD_TOKEN"]
GUILD_ID = discord.Object(id=int(os.environ["GUILD_ID"]))

# Intents par défaut : tout sauf les privilégiés. Suffisant pour les slash commands.
intents = discord.Intents.default()


class MonBot(commands.Bot):
    def __init__(self):
        # command_prefix est obligatoire mais ne servira pas : on n'utilise que des slash commands
        super().__init__(command_prefix="!", intents=intents)

    async def setup_hook(self):
        # Appelé une fois, avant la connexion. On synchronise les commandes sur le serveur de test :
        # elles apparaissent immédiatement, contrairement aux commandes globales (jusqu'à 1 h).
        self.tree.copy_global_to(guild=GUILD_ID)
        synced = await self.tree.sync(guild=GUILD_ID)
        print(f"{len(synced)} commande(s) synchronisée(s) sur le serveur de test")


bot = MonBot()


@bot.event
async def on_ready():
    print(f"Connecté : {bot.user} (id {bot.user.id})")


@bot.tree.command(name="ping", description="Vérifie que le bot répond")
async def ping(interaction: discord.Interaction):
    # bot.latency : latence de la connexion WebSocket, en secondes
    await interaction.response.send_message(f"Pong ! Latence WebSocket : {round(bot.latency * 1000)} ms")


@bot.tree.command(name="roll", description="Lance un dé")
@app_commands.describe(faces="Nombre de faces, 6 par défaut")
async def roll(interaction: discord.Interaction, faces: app_commands.Range[int, 2, 1000] = 6):
    # Range[int, 2, 1000] : Discord refuse lui-même les valeurs hors bornes, votre code n'est pas appelé
    resultat = random.randint(1, faces)
    await interaction.response.send_message(f"Dé à {faces} faces : {resultat}")


@bot.tree.command(name="user", description="Affiche depuis quand un membre est sur le serveur")
@app_commands.describe(membre="Le membre à inspecter, vous par défaut")
async def user(interaction: discord.Interaction, membre: discord.Member | None = None):
    membre = membre or interaction.user
    depuis = membre.joined_at.strftime("%d/%m/%Y")
    await interaction.response.send_message(f"{membre.display_name} est sur le serveur depuis le {depuis}.")


bot.run(TOKEN)

Trois choses à remarquer. Les paramètres de la fonction deviennent les options de la commande, avec leur type : Discord affiche un champ numérique pour faces et un sélecteur de membre pour membre. La réponse passe toujours par interaction.response, jamais par un envoi direct dans le salon. Et bot.run est la dernière ligne : tout ce qui vient après ne s'exécutera jamais.

Lancez avec python bot.py. Vous devez voir « 3 commande(s) synchronisée(s) » puis « Connecté : MonBot#1234 ». Sur Discord, tapez / : les trois commandes sont là.

7. Synchroniser les commandes avec Discord

C'est le point qui déroute quand on vient des commandes à préfixe : Discord doit connaître vos slash commands avant de les afficher. En discord.py, ça s'appelle synchroniser l'arbre de commandes, bot.tree.sync(). Le code ci-dessus le fait dans setup_hook, pour le serveur de test seulement.

Serveur de test ou global ? sync(guild=GUILD_ID) ne touche qu'un serveur et prend effet tout de suite. Pour ouvrir le bot à tout le monde, appelez await self.tree.sync() sans argument : les commandes deviennent globales, avec jusqu'à une heure de délai. Discord limite le nombre de synchronisations par jour, donc en production, ne synchronisez que quand une commande change, par exemple via une commande réservée au propriétaire, plutôt qu'à chaque démarrage.

8. La règle des trois secondes

Discord attend une réponse pendant trois secondes. Au-delà, l'utilisateur voit « L'application ne répond pas » et votre réponse tardive est ignorée. Pour une commande qui interroge une API ou une base de données, on prévient Discord tout de suite avec defer, puis on répond via followup, avec quinze minutes de marge.

Séquence : l'utilisateur tape /ping, Discord envoie une interaction au bot, le bot a trois secondes pour répondre ou différer, puis le message apparaît
Une commande qui a du travail
import aiohttp


@bot.tree.command(name="meteo", description="Exemple de traitement long")
@app_commands.describe(ville="Nom de la ville")
async def meteo(interaction: discord.Interaction, ville: str):
    await interaction.response.defer()  # « le bot réfléchit... », envoyé en quelques ms

    async with aiohttp.ClientSession() as session:  # aiohttp, pas requests : on ne bloque pas la boucle
        async with session.get(f"https://api.exemple.fr/meteo?ville={ville}") as reponse:
            donnees = await reponse.json()

    await interaction.followup.send(f"Il fait {donnees['temperature']} °C à {ville}.")  # jusqu'à 15 min après

9. Organiser le code en cogs

Un fichier unique tient jusqu'à cinq ou six commandes. Au-delà, discord.py propose les cogs : des classes qui regroupent des commandes et des événements liés, chargées depuis des fichiers séparés. Quand une commande casse, vous ne touchez qu'un fichier. Voici /ping et /user déplacées dans un cog :

cogs/utilitaires.py
import discord
from discord import app_commands
from discord.ext import commands


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

    @app_commands.command(name="ping", description="Vérifie que le bot répond")
    async def ping(self, interaction: discord.Interaction):
        await interaction.response.send_message(f"Pong ! {round(self.bot.latency * 1000)} ms")

    @app_commands.command(name="user", description="Affiche depuis quand un membre est sur le serveur")
    @app_commands.describe(membre="Le membre à inspecter, vous par défaut")
    async def user(self, interaction: discord.Interaction, membre: discord.Member | None = None):
        membre = membre or interaction.user
        await interaction.response.send_message(
            f"{membre.display_name} est sur le serveur depuis le {membre.joined_at:%d/%m/%Y}."
        )


# Fonction appelée par bot.load_extension("cogs.utilitaires")
async def setup(bot: commands.Bot):
    await bot.add_cog(Utilitaires(bot))

Et dans bot.py, on charge les cogs avant de synchroniser, sinon leurs commandes ne seront pas envoyées à Discord :

bot.py, setup_hook complété
    async def setup_hook(self):
        for extension in ("cogs.utilitaires", "cogs.fun"):
            await self.load_extension(extension)

        self.tree.copy_global_to(guild=GUILD_ID)
        synced = await self.tree.sync(guild=GUILD_ID)
        print(f"{len(synced)} commande(s) synchronisée(s) sur le serveur de test")

10. Réagir aux événements : le message de bienvenue

Les événements arrivent sans qu'on les demande : un membre rejoint, un message est supprimé. Le message de bienvenue est l'exemple classique, et il demande l'intent members, qui est privilégié. Deux gestes : cocher Server Members Intent dans le portail (onglet Bot), puis dans le code :

bot.py, deux ajouts
# Remplace la ligne des intents
intents = discord.Intents.default()
intents.members = True  # privilégié : à cocher aussi dans le portail développeur


# N'importe où après la création du bot
@bot.event
async def on_member_join(member: discord.Member):
    salon = member.guild.system_channel  # le salon « système » des paramètres du serveur
    if salon is not None:
        await salon.send(f"Bienvenue {member.mention} ! Pense à lire les règles avant de poster.")

Si la case du portail est oubliée, le bot s'arrête au démarrage sur discord.errors.PrivilegedIntentsRequired, avec un message qui explique exactement quoi faire. C'est l'une des erreurs les mieux rédigées de la bibliothèque, et pourtant la plus fréquente.

11. Le garder en ligne 24h/24

Le bot tourne tant que le terminal est ouvert. Pour qu'il survive à la fermeture du PC, il lui faut une machine qui ne s'éteint pas : un VPS que vous administrez, ou un hébergeur spécialisé avec un panel. Les trois options sont comparées dans le guide sur l'hébergement.

Sur un panel comme le nôtre, en environnement Python, ce qui change par rapport à votre machine :

  • Le serveur lit requirements.txt et installe les dépendances au premier démarrage. N'envoyez pas le dossier .venv.
  • Le .env est remplacé par les variables de l'onglet Startup : mêmes noms, mêmes valeurs. os.environ les lit sans rien changer au code.
  • Le fichier de démarrage se règle dans le même onglet : indiquez bot.py si ce n'est pas la valeur par défaut. La version de Python se change depuis le panel sans recréer le serveur.

Un bot comme celui de ce guide tient dans le plan gratuit à 128 Mo. Ce qui le ferait déborder, et quand passer à une offre payante, est détaillé dans gratuit vs payant.

12. Sécurité et bonnes habitudes

  • Le token ne va jamais dans le code ni sur Git. Discord surveille les dépôts publics et révoque ce qu'il y trouve. Le symptôme : un bot qui tombe « sans raison ». Reset Token, et on repart.
  • Permissions minimales à l'invitation, et @app_commands.default_permissions(manage_messages=True) sur les commandes sensibles pour les cacher à ceux qui n'ont pas le droit.
  • Jamais de code bloquant dans une fonction async : time.sleep, requests.get ou un gros calcul figent tout le bot le temps qu'ils finissent. asyncio.sleep, aiohttp, et asyncio.to_thread pour le reste.
  • Double authentification sur le compte Discord propriétaire de l'application. Qui contrôle le compte contrôle le bot.
  • Figez les versions dans requirements.txt (c'est ce que fait pip freeze) : ce qui tourne chez vous tournera pareil chez l'hébergeur.

13. Dépannage

SymptômeCause probableCorrectif
PrivilegedIntentsRequiredIntent privilégié activé dans le code, pas dans le portail.Onglet Bot du portail, cocher l'intent. Ou le retirer du code.
LoginFailure: Improper token has been passedToken mal copié, régénéré, ou variable vide.Vérifier le .env (pas d'espace ni de guillemets), ou Reset Token.
ModuleNotFoundError: No module named 'discord'Environnement virtuel pas activé, ou pip a installé ailleurs.Activer le .venv, puis pip install -r requirements.txt. Sur un hébergeur : vérifier requirements.txt.
Les commandes n'apparaissent pasPas de sync, mauvais GUILD_ID, cogs chargés après le sync, ou scope applications.commands absent.Lire le message de sync au démarrage, vérifier l'identifiant, charger les cogs avant sync, réinviter le bot.
« L'application ne répond pas »Plus de trois secondes avant la réponse, ou une exception avant.interaction.response.defer() en début de commande, et lire la console.
Forbidden: 403 ... Missing PermissionsLe bot n'a pas le droit d'écrire dans ce salon, ou son rôle est trop bas.Permissions du salon et ordre des rôles.
TypeError: unsupported operand type(s) for |Python plus ancien que 3.10 : la syntaxe « Member | None » n'existe pas.Mettre Python à jour, ou utiliser Optional[discord.Member].
Le bot « gèle » quelques secondesUn appel bloquant (time.sleep, requests) dans une fonction async.Remplacer par asyncio.sleep, aiohttp, ou asyncio.to_thread.

14. Questions fréquentes

Quelle version de Python faut-il ?

Python 3.10 au minimum, et de préférence 3.12. Le code de ce guide utilise la syntaxe de type « discord.Member | None », qui n'existe pas avant 3.10. Vérifiez avec python --version.

discord.py est-il toujours maintenu ?

Oui. Le projet a été mis en pause en 2021 puis a repris en 2022 avec la version 2.0, qui a ajouté le support des slash commands via app_commands. Les versions 2.x sortent régulièrement.

Python ou JavaScript pour un premier bot ?

Les deux conviennent. Si vous connaissez déjà un des deux, prenez celui-là. discord.py est un peu plus court à écrire, discord.js a plus d'exemples en ligne. Le guide JavaScript est lié en bas de page.

Pourquoi mes slash commands n'apparaissent pas ?

Soit la synchronisation n'a pas eu lieu (regardez le message « commande(s) synchronisée(s) » au démarrage), soit le GUILD_ID ne correspond pas au serveur où vous testez, soit le bot a été invité sans le scope applications.commands. Une commande globale peut mettre jusqu'à une heure à apparaître.

Dois-je synchroniser les commandes à chaque démarrage ?

Pendant le développement, synchroniser sur le serveur de test à chaque démarrage est pratique et sans risque. En production, avec des commandes globales, synchronisez seulement quand vous ajoutez ou modifiez une commande : Discord limite le nombre de synchronisations par jour.

Les commandes à préfixe comme !ping marchent encore ?

Oui, via commands.Bot et le décorateur @bot.command, mais elles exigent l'intent message_content, qui est privilégié. Les slash commands n'en ont pas besoin, c'est pour ça que ce guide les utilise.

Que faire si mon token a fuité ?

Portail développeur, onglet Bot, Reset Token. L'ancien cesse immédiatement de fonctionner. Mettez le nouveau dans le .env ou dans les variables de votre hébergeur et redémarrez.

Combien de RAM consomme un bot discord.py ?

Un bot avec les intents par défaut et quelques commandes tourne confortablement sous les 128 Mo. La consommation grimpe avec l'intent members sur de gros serveurs, les caches de messages et les traitements d'images ou d'audio.

Et maintenant

Vous avez un bot organisé en cogs, qui répond à des slash commands, accueille les nouveaux membres et ne bloque pas sa boucle d'événements. La suite dépend de votre idée : des boutons et des menus (discord.ui), une base de données pour retenir des choses, ou la mise en ligne pour que tout ça tourne sans vous.

La documentation de discord.py est la référence pour aller plus loin, et ses exemples dans le dossier examples/app_commands du dépôt reprennent exactement les mécanismes vus ici.

À lire ensuite

Le bot est prêt. Reste à le laisser tourner.

Le plan gratuit donne 128 Mo de RAM, un demi vCore Intel Gold et 512 Mo de SSD, avec Python 3.10 à 3.13 au choix. Serveur livré en 120 secondes, sans carte bancaire.