How to Create a Basic Discord Bot [discord.py]

Zane Schenk
9 min readJul 21, 2023

--

Photo by Mariia Shalabaieva on Unsplash

Resources for creating Discord bots are few and far between. On top of that, Discord’s functionalities are constantly changed or added to, deterring users from creating fun or useful bots for themselves and their communities. As a current Discord bot developer managing a bot, database, and hundreds of users, I’ve put together a guide containing the basics as well as a few tips from my own experience. This guide will walk you through creating a Discord bot from scratch using discord.py.

Getting Started:

NOTE: this will require that you have a verified Discord account as well as two-factor authentication with Google Authenticator.

  • Click “New Application” in the upper right-hand corner, give your bot a name, and agree to the Developer ToS.

Congrats! You’ve created your first bot! Now let’s invite it to a guild and give it functionality!

Inviting Your Bot:

  • Click “OAuth2” in the left column and select “URL Generator” from the sub-options.
  • While this menu may seem overwhelming, the setup is quite simple! In the “scopes” section simply check “bot”. This tutorial utilizes the new Interactions API and built-in slash commands. To enable these, also check “applications.commands”.
  • “Bot permissions” are up to you! I personally give my bot administrative control because it needs almost every option to function properly. However, if you don’t plan to utilize all of Discord’s features, only check what you believe will be required for your functionality.
  • Copy/paste the URL that’s generated at the bottom of the page and visit it in your browser! Choose the guild you want to invite it to and then allow it to join!
    NOTE: You may also want to save this for future invites.

Programming Strategy:

While this tutorial uses discord.py, there are lots of ways to code Discord bots. You can code everything manually with API calls or you can utilize existing easy to use libraries. The two mainstream libraries are:

Pre-Requisites [discord.py]:

  • Install Python
  • Install an IDE and configure it for Python. I personally use Visual Studio Code
  • Install discord.py using PIP:
#Linux/macOS:
python3 -m pip install -U discord.py
#Windows:
py -3 -m pip install -U discord.py

ALTERNATIVE OPTION [Includes Voice Support]:

#Linux/macOS:
python3 -m pip install -U "discord.py[voice]"
#Windows:
py -3 -m pip install -U discord.py[voice]
  • Create a new file: yourBotName.py (or your preferred naming convention).

Now it’s time to get programming!

Code Walkthrough:

To get started, you first need to import the required libraries and classes:

import discord, discord.utils
from discord import app_commands

Intents:
Discord uses “intents” which are basically bot permissions that may need to be turned on depending on what you’re doing. Here are some examples of the most common ones :

#INTENTS 
intents = discord.Intents.all() #Grabs default intents
intents.members = True #Changes member visibility intent (allows the bot to “see” members)
intents.presences = True #Changes presence intent (allows you to set a custom bot presence)

Bot as a Class:
While not required, it can be helpful to make your Discord bot an object. You may even use a hierarchy of objects, but a basic implementation of a bot class may look like this:

#BOT CLASS
class bot(discord.Client):
def __init__(self):
super().__init__(intents = intents) #Set the bot’s intents
self.synced = False #Default synced value

async def on_ready(self): #Bot on_ready function
await self.wait_until_ready() #Wait until the bot is ready.
if not self.synced:
await tree.sync(guild = discord.Object(id = your_guild_id)) #EXPLANATION BELOW
self.synced = True #Update synced variable
self.change_presence(status = discord.Status.online, activity = discord.Game(your_status_message_here)) #Sets custom activity status
print("your_bot_name_here is Ready.") #Prints ready message

Sync Types:
You’ll notice I specify one guild in the tree.sync function call. This is very important for testing purposes. Discord allows you to run either global sync or singular sync (syncs to one guild). Running a global sync can take an hour or two to show-up for use. When coding/testing use a single guild and when deploying, simply use:

await tree.sync()

CommandTrees:
Next you need to create a bot object as well as a CommandTree for it:

bot = bot() #Initialize a bot object
tree = app_commands.CommandTree(bot) #Initialize a CommandTree

Token and Execution:
Lastly, retrieve your token and use it to run your bot.
NOTE: You will need to go to the “Bot” option in the left column of the web portal and click “Reset Token”. This is the token that will connect your code to your Discord bot. Do not share this with anyone!

#RUN YOUR BOT
bot.run(your_token_here)

Congratulations! You have a functioning bot! Now I’ll explain how to implement some basic functionality!

Important Information:

Before I go over the specifics of the “fun” code, it’s important to understand how Discord bots work. Here are some things you should know and/or consider before you add features to your bot.

Events:
Discord’s servers (that will redirect to your code) listen for “events”. Events are triggers that cause a bot to respond in some way. While there are many, the following are the most common triggers that bots respond to:

  • A member sending a message
  • A member invoking a slash command
  • Your bot joining a new guild

Each of these triggers is programmed differently. The following function decorators and signatures are used for each of the above cases:

#ON MESSAGE EVENTS
@bot.event
async def on_message(message: discord.Message):
...your functionality here

#SLASH COMMAND EVENTS
@tree.command(description = "This is a command description!")
async def command_name(interaction: discord.Interaction):
...your functionality here

#BOT JOINING NEW GUILD EVENTS
@bot.event
async def on_guild_join(guild: discord.Guild):
...your functionality here

Code Structure:
Keep in mind, there is only one each of the on_message and on_guild_join functions. Slash commands will have one function per command or sub command. These will be up to you to implement the way you want.

Storing Data:
It’s important to consider the need for storing user or bot data and how you may do this. Reading and writing to text files is an option, dictionaries will make this easy. But problems will arise with concurrency and speed requirements if you scale the bot to handle more users and guilds.

The easiest solution is to use a database that constantly runs. This can be hosted using a third-party service or you can host one yourself on a power efficient device such as a Raspberry Pi or laptop. There are many types of databases and flavors of SQL that each uses. For my own production I’ve used a Raspberry Pi running MariaDB (a light-weight MySQL fork). This will, again, be up to you to research if you need to store large amounts of data for your bot.

Slash Commands:

Now that you’ve considered how you want to invoke your functionality and where to store potential results, let me explain how slash commands work.

Slash commands use the your_tree_name.command() decorator above optional permissions decorators and then have a signature in the format:

#BASIC SIGNATURE
async def command_name(interaction: discord.Interaction, parameters...):

Parameters:
You can include addition parameters as needed that will need to be specified when a user calls the command. Primitive datatypes are self explanatory but one of the main functionalities I struggled to find documentation on were lists of options that can be selected from. Here are some examples of common implementations you might be want:

#SIGNATURE WITH SPECIFIC EXAMPLES
async def command_name(interaction: discord.Interaction, role: Optional[discord.Role], member: Optional[discord.Member], mode: Literal["Hello", "World"]):

In this above example, I included the syntax for using options, Role and Member selection, and literal lists. There are other discord classes that you can do use in selection panes. I hope this code snippet is helpful!

Permissions [Optional]:
The optional permissions decorators allow you to restrict who can run the command. The argument must evaluate True for an app_command check to proceed with execution. An example I pulled from the official discord.py website is below:

#LAMBDA EVALUATION FUNCTION
def check_if_it_is_me(interaction: discord.Interaction) -> bool:
return interaction.user.id == 85309593344815104 #Checks user id of invoker against a constant user id

#SLASH COMMAND
@tree.command()
@app_commands.check(check_if_it_is_me) #Variable or called function must be true for command to execute
async def only_for_me(interaction: discord.Interaction):
await interaction.response.send_message('I know you!', ephemeral = True) #Sends ephemeral response

Responses:
It’s important that you always include a discord.Interaction as your first parameter as well as understand how Discord Interactions work. Interactions require a “response”. So at some point in the command, you will need to include message response. Response can be sent either right away if your command is quick, or if it takes processing time, you may need to defer the response and follow-up with it later:

#STANDARD RESPONSE
await interaction.response.send_message("I'm a response")

#DELAYED RESPONSE
await interaction.response.defer() #Defers response
await interaction.followup.send("I'm a response") #Follows up with a deferred response

Either way, the bot will respond with a message upon command execution. Responses should usually be ephemeral (sent as a blue message that only the executor can see). You can simply add the ephemeral argument after your string in the send or send_message functions.

Finishing Up:

You’ve now added a command and you’re ready to run/test your bot! Congrats! If you have additional questions, the Interactions API Reference is the best place to start; followed by online forums. If you have any additional suggestions for future articles or edits, please let me know!

Here are some general bot development tips based on my developer experience:

  • Creating a second application on the Discord developers portal can allow you to run production code on one application while running a private build on another simultaneously. This is necessary if you don’t want to incur application downtime when developing.
  • Using a development bool to flip back and forth between running production and testing instances of your bot may be useful.
  • MySQL databases are easier using the MySQL Python Connector though you may still need a custom SQL controller or lots of utility functions.
  • Refactoring and recycling code as well as making additional files is extremely important as projects can grow fast! The bot I’m still developing is nearing 2000 lines of code!

Finalized Code:

import discord, discord.utils
from discord import app_commands

#INTENTS
intents = discord.Intents.all() #Grabs default intents
intents.members = True #Changes member visibility intent (allows the bot to "see" members)
intents.presences = True #Changes presence intent (allows you to set a custom bot presence)

#BOT CLASS
class bot(discord.Client): #Bot class
def __init__(self):
super().__init__(intents = intents) #Set the bot's intents
self.synced = False #Default synced value

async def on_ready(self): #Bot on_ready function
await self.wait_until_ready() #Wait until the bot is ready.
if not self.synced:
await tree.sync(guild = discord.Object(id = your_guild_id)) #EXPLANATION BELOW
self.synced = True #Update synced variable
self.change_presence(status = discord.Status.online, activity = discord.Game(your_status_message_here)) #Sets custom activity status
print("your_bot_name_here is Ready.") #Prints ready message

#ON MESSAGE EVENTS
@bot.event
async def on_message(message: discord.Message):
...your functionality here

#SLASH COMMAND EVENTS
@tree.command(description = "This is a command description!")
async def command_name(interaction: discord.Interaction):
await interaction.response.send_message("I'm a response")

#SLASH COMMAND EVENTS
@tree.command(description = "This is second command's description!")
@app_commands.check(check_if_it_is_me) #Variable or called function must be true for command to execute
async def second_command_name(interaction: discord.Interaction):
await interaction.response.send_message('I know you!', ephemeral = True) #Sends ephemeral response

#BOT JOINING NEW GUILD EVENTS
@bot.event
async def on_guild_join(guild: discord.Guild):
...your functionality here

#LAMBDA EVALUATION FUNCTION
def check_if_it_is_me(interaction: discord.Interaction) -> bool:
return interaction.user.id == 85309593344815104 #Checks user id of invoker against a constant user id

#INITIALIZATION
bot = bot() #Initialize a bot object
tree = app_commands.CommandTree(bot) #Initialize a CommandTree

#RUN YOUR BOT
bot.run(your_token_here)

I hope that you found this article concise and informative! Thank you for reading and have fun in development!

--

--