Kamol Nazarov The back office lab Brooklyn, NY

Lab notes · N°03 · · 3 min read

Fixing ModuleNotFoundError When a Python Cron Job Works From the Shell

Same script, same machine: works over ssh, ModuleNotFoundError from cron. Cron never activates your venv - point the entry at the venv's own python.

Written by
Kamol Nazarov
Published
Tags
python · cron · venv
Follow
RSS feed

The note

The nightly job pulls the day's repair orders into a spreadsheet for the front desk. Tested it over ssh before bed: clean run, rows in the sheet. At 2:30 the next morning cron emailed me this instead:

Traceback (most recent call last):
  File "/home/shop/order-sync/sync.py", line 3, in <module>
    import requests
ModuleNotFoundError: No module named 'requests'

If you're searching "python cron job module not found" or "python script works in terminal but not cron", this is that situation exactly: same machine, same script, same user � but only cron can't find the modules. There's really one cause, one fix, and two neighboring traps that ride along with it.

What cron actually runs

Cron is not your login shell. It launches each entry with a nearly empty environment: SHELL=/bin/sh, a minimal PATH (typically /usr/bin:/bin), your HOME, and that's about it. It never reads .bashrc, .profile, or anything you've exported by hand.

Two consequences do the damage together:

  1. Whatever you installed into a virtualenv is invisible, because nothing ever ran source venv/bin/activate.
  2. The bare python3 in your crontab resolves through that minimal PATH to the system interpreter, whose site-packages don't contain your packages.

System Python plus missing venv equals ModuleNotFoundError, every time.

Prove it in one line

Before fixing anything, confirm which interpreter cron is really using. Drop this at the top of the script temporarily:

import sys
print(sys.executable)
print("\n".join(sys.path))

Cron mails command output to you (or your entry redirects to a log � it should). You'll see /usr/bin/python3 where you expected /home/shop/order-sync/venv/bin/python3. That mismatch is the entire bug.

The fix nobody tells you: skip activation entirely

Most threads tell you to source activate in a wrapper script. You can � but activation is optional. A venv is just a directory: a copy of the interpreter and its own site-packages, with a pyvenv.cfg beside the binary. The interpreter locates its venv from its own path, not from environment variables. Run the venv's python directly and you get the venv, no activation, no PATH setup, from any shell, service, or container.

So this is how to run a python venv in crontab without the ceremony � point the entry at the venv's own interpreter:

30 2 * * * /home/shop/order-sync/venv/bin/python3 /home/shop/order-sync/sync.py >> /home/shop/order-sync/cron.log 2>&1

One line, absolute paths, done. That version survives a server rebuild and a migration to systemd later, because it depends on nothing outside the entry itself.

The two traps that ride along

The wrong interpreter explains the import error, but a bare crontab entry tends to collect two more failures on the same night:

Symptom Cause Fix
ModuleNotFoundError System interpreter ran instead of the venv Full path to venv/bin/python3
FileNotFoundError on a config or data file Cron starts in $HOME, not your project dir; relative paths break cd /project && before the command, or absolute paths
sh: pg_dump: command not found from a subprocess call Minimal PATH misses /usr/local or custom bins PATH= line at the top of the crontab

The hardened version handles all three:

SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
30 2 * * * cd /home/shop/order-sync && ./venv/bin/python3 sync.py >> cron.log 2>&1

The PATH= and SHELL= lines apply to every entry below them in the same crontab. The cd ... && guard is what keeps open("orders.csv") resolving to the file you meant.

Test it the way cron will run it

You can reproduce cron's empty environment without waiting for 2:30 AM:

env -i HOME="$HOME" SHELL=/bin/sh /bin/sh -c \
  '/home/shop/order-sync/venv/bin/python3 /home/shop/order-sync/sync.py'

env -i wipes your shell's variables, so the command sees exactly the stripped world cron provides � same PATH emptiness, same working directory rules. If it works there, it works in the crontab; if it doesn't, the error you get is the real one, not one polluted by your interactive session.

One sentence to keep: cron never activates anything � it runs /bin/sh with a scrubbed environment, so point the entry at the venv's own interpreter and use absolute paths. After that, "crontab python no module named" stops being a mystery and your 2:30 AM email goes back to being a receipt.