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.
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.
- 1
New Application, un nom, et c'est tout.
- 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
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
OAuth2, URL Generator. Scopes
botetapplications.commands, le second étant obligatoire pour les slash commands. Permissions : Send Messages, Embed Links, Read Message History. Pas Administrator. - 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.
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 :
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 :
DISCORD_TOKEN=le_token_copié
GUILD_ID=identifiant_serveur_test.venv/
__pycache__/
.envmon-bot/
├── .env
├── .gitignore
├── requirements.txt
├── bot.py # point d'entrée : connexion, chargement des cogs, synchronisation
└── cogs/
├── utilitaires.py # /ping, /user
└── fun.py # /roll6. 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 :
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.
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ès9. 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 :
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 :
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 :
# 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.txtet installe les dépendances au premier démarrage. N'envoyez pas le dossier.venv. - Le
.envest remplacé par les variables de l'onglet Startup : mêmes noms, mêmes valeurs.os.environles lit sans rien changer au code. - Le fichier de démarrage se règle dans le même onglet : indiquez
bot.pysi 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.getou un gros calcul figent tout le bot le temps qu'ils finissent.asyncio.sleep,aiohttp, etasyncio.to_threadpour 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ôme | Cause probable | Correctif |
|---|---|---|
| PrivilegedIntentsRequired | Intent 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 passed | Token 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 pas | Pas 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 Permissions | Le 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 secondes | Un 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 même guide avec discord.js
Pour ceux qui préfèrent JavaScript : SlashCommandBuilder, deploy-commands.js et la Collection de commandes.
GuideComment héberger un bot Discord en 2026
PC, VPS ou hébergeur spécialisé : les trois options comparées, puis la mise en ligne pas à pas sur le panel.
ComparatifGratuit vs payant : que choisir ?
Ce qui tient dans 128 Mo de RAM, et à quel moment passer à Standard ou Pro.
TutorielDéployer un bot sur le panel en 5 minutes
Écrit pour discord.js, mais les onglets Fichiers, Startup et Console sont les mêmes en Python.