Quick answer
Cron runs with a minimal environment, so Python can't find your modules. Fix it by setting the full path to your Python interpreter, activating your virtualenv inside the cron command, and exporting the right PYTHONPATH — or just wrap the whole thing in a shell script that sources your environment first.
Why this happens
You've tested your script a dozen times in the terminal. You type python3 my_script.py, it runs, everyone's happy. Then you drop it in crontab, wait five minutes, and get an email saying ModuleNotFoundError: No module named 'requests'. Frustrating, right?
Here's the deal. Cron doesn't load your shell profile. It doesn't source .bashrc, it doesn't know about your virtualenv, and it sure as hell doesn't know where pip put your packages. When you run a command in your terminal, you inherit your user's environment — PATH, VIRTUAL_ENV, everything. Cron gets a bare-bones environment with PATH=/usr/bin:/bin and nothing else. So when cron fires up python3, it's using the system Python, not the one from your venv where you installed requests.
I had a client last month whose nightly backup script died for three weeks straight because of this. They'd installed everything under a pyenv virtualenv, and cron kept picking up /usr/bin/python3. Nobody noticed until the disk filled up.
Step-by-step fix
1. Find the full path to your Python interpreter
Inside the environment where your script works, run:
which python3
# e.g. /home/youruser/venv/bin/python3
Copy that path. You'll need it in a second.
2. Update your crontab to use the explicit interpreter
Open your crontab with crontab -e and change the command from this:
*/5 * * * * python3 /home/youruser/scripts/my_script.py
To this:
*/5 * * * * /home/youruser/venv/bin/python3 /home/youruser/scripts/my_script.py
That alone fixes 70% of these cases. The interpreter now knows where its own site-packages live.
3. Set PYTHONPATH if your modules live outside site-packages
If you're importing your own .py files from a sibling directory, you need to tell Python where to look. Add this line at the top of your crontab:
PYTHONPATH=/home/youruser/myproject
*/5 * * * * /home/youruser/venv/bin/python3 /home/youruser/scripts/my_script.py
Multiple paths? Separate them with colons, just like PATH.
4. Set PATH and HOME too — cron is dumb
Some modules shell out to other binaries, and without a sane PATH they'll fail too. Throw this at the top of the crontab:
PATH=/home/youruser/venv/bin:/usr/local/bin:/usr/bin:/bin
HOME=/home/youruser
PYTHONPATH=/home/youruser/myproject
Yes, you can set env vars in crontab. They apply to every job below them.
5. Test with the exact cron environment
Don't guess. Reproduce cron's stripped environment manually:
env -i /home/youruser/venv/bin/python3 /home/youruser/scripts/my_script.py
env -i clears everything. If this works, cron will work. If it doesn't, you now see the real error without waiting five minutes.
Alternative fixes if that doesn't cut it
Wrap it in a shell script
This is my go-to for anything non-trivial. Create /home/youruser/scripts/run_backup.sh:
#!/bin/bash
source /home/youruser/venv/bin/activate
export PYTHONPATH=/home/youruser/myproject
cd /home/youruser/myproject
python my_script.py
chmod +x it, then point cron at the script. Now your environment setup lives in a file you can actually read and debug instead of a cramped crontab line.
Use the shebang line
If your script starts with #!/home/youruser/venv/bin/python3, you can call it directly from cron. Just make sure the file is executable. This is clean but brittle if you ever move the venv.
Switch to systemd timers
Honestly? On modern Linux, systemd timers are a better fit for anything important. They let you specify Environment=, WorkingDirectory=, and User= cleanly, and you get proper logging with journalctl. Cron is fine for quick stuff, but I've moved three clients off it this year for critical jobs.
Prevention tip
Never rely on cron inheriting your shell environment — it never will. Every cron job that runs Python should have an explicit interpreter path and a PYTHONPATH set at the top of the crontab (or in a wrapper script). And before you deploy, run env -i against the exact command. If it passes that test, you won't get a 3 AM email about a missing module.