What No module named 'discord' is telling you
The library is installed from PyPI as discord.py and imported as discord. The error means the Python interpreter that started your bot looked for a module called discord and did not find one. The package may be installed somewhere, just not for that interpreter.
On a server, the usual reason is that nothing installed it: there was no requirements.txt, it had a different name, or it sat in a subfolder the host did not look in. On your own computer, the usual reason is two Pythons. You installed discord.py with one, through a pip that belongs to another installation or outside your virtual environment, and run the bot with the other. Running pip as python -m pip, which pip's guide recommends, ties the install to the exact interpreter you name.
One look-alike is worth knowing. If your own script is called discord.py, import discord finds your file first, because Python searches the directory containing the script before anything else. The error is then about an attribute or a partially initialised module rather than a missing one. Rename the file, for example to bot.py, and delete any __pycache__ folder beside it.
- Install with python -m pip install -U discord.py (on Windows, py -3 -m pip install -U discord.py)
- The PyPI package named discord describes itself as a mirror package for discord.py and asks you to install discord.py instead
- Check which Python runs the bot with python -c "import sys; print(sys.executable)"
- Never name your own file discord.py
Write a requirements.txt the server can install
A requirements file is a list of packages for pip install -r to install. Create it at the root of the project, named exactly requirements.txt, so a host that installs dependencies on start can find it.
Pin the versions. pip's user guide describes the standard way: run pip freeze and save the output, which records a pinned version of everything installed, for repeatable installs. Do it from inside a virtual environment that holds only this bot's packages. Outside one, pip freeze lists every package on your machine, including ones the bot never imports.
An unpinned line such as discord.py installs the newest release on the day the server builds, which may not be the version you tested. Pinned lines such as discord.py==2.7.1 install the same thing everywhere, and an upgrade becomes a change you choose and test.
- python -m venv .venv, then activate it: source .venv/bin/activate, or .venv\Scripts\activate on Windows
- python -m pip install -U discord.py and everything else the bot imports
- python -m pip freeze > requirements.txt
- Commit requirements.txt. Do not commit the .venv folder: add it to .gitignore
- Voice support needs the extra: discord.py[voice]. On Linux, discord.py's docs also list libffi, libnacl and python3-dev
Read the token from the environment, not from a file
Discord's developer quick start says never to check a token into any kind of version control, and a token in a source file ends up there sooner or later. The standard pattern is to read it from an environment variable, which the host sets outside your code.
In Python that is os.environ. Prefer os.environ["DISCORD_TOKEN"] to os.getenv("DISCORD_TOKEN"): the first raises a KeyError naming the missing variable the moment the bot starts, while the second quietly returns None and lets the bot fail later with a less obvious error. The Python docs add one detail: os.environ is captured when the os module is first imported, so set variables before the process starts, not while it runs.
For local development, python-dotenv's load_dotenv reads a .env file into os.environ. By default it does not override variables that already exist, so the same code uses your .env at home and the host's real environment on the server. Its README suggests adding .env to .gitignore, and that should happen before the first commit.
- token = os.environ["DISCORD_TOKEN"], then client.run(token)
- .env holds DISCORD_TOKEN=... for local runs only, and is listed in .gitignore
- Commit a .env.example with the variable names and no values, so the next person knows what to set
- If the token was ever committed, reset it on the Bot page of the Developer Portal before anything else
Errors that look like a broken deploy but are not
Once the packages install, the next failures come from Discord, not from Python, and restarting will not clear them.
LoginFailure, with the message Improper token has been passed, means the token is wrong: an old value after a reset, a value with stray quotes or spaces copied into the variable, or the application's client secret pasted instead of the bot token. PrivilegedIntentsRequired means the code asks for an intent, such as message_content or members, that is not switched on for the bot in the Developer Portal. And a bot that connects but ignores prefix commands is usually missing the Message Content intent altogether.
These and the other reasons a bot drops off are covered in Discord bot keeps going offline.
- LoginFailure: copy a fresh token from the Bot page into the environment variable, with no quotes
- PrivilegedIntentsRequired: enable the intent on the Bot page, or remove it from your code
- Connected but silent: set intents.message_content = True and enable Message Content in the portal
Match the Python version and the start command
discord.py requires Python 3.8 or higher. Develop on the same minor version the server runs, because a version pinned while testing on one Python may not install the same way, or at all, on another.
Start the bot with one plain foreground command such as python bot.py, with client.run(token) as the last call. discord.py's documentation describes run() as a blocking call that must be the last function you call, and it also sets up logging so the library's messages reach the console.
Keep anything the bot must remember in a file or database on persistent storage rather than in variables. A restart clears memory, and on a supervised host a crash means a restart. For the wider picture of keeping a bot up, see how to host a Discord bot 24/7.
The same bot on Puranode
Puranode's Python runtime runs on Python 3.10, 3.11 or 3.12, and installs requirements.txt each time the bot starts. That makes pinning the step that matters: a pinned file installs the same versions on every restart.
Deploy from a Git repository or upload your files in the panel or over SFTP, then set DISCORD_TOKEN and any API keys as container environment variables in the panel. They stay out of your code and out of your repository, and os.environ reads them like any other variable. The console streams the bot's output live, so a missing package or a LoginFailure shows up there within seconds of starting. If your bot needs system libraries beyond what Python packages provide, email hello@puranode.com before ordering. The bot hosting page lists the plans.