How to Write a Cron Job in Python: Steps, Code and Traps
Sagar Tayde · · 8 min read
To write a cron job in Python, you write a normal script, then add one line to your crontab that runs it with the full path to your Python interpreter. There are two other ways too: write the crontab from Python code, or keep a scheduler running inside your program. Each one fails in its own way.
This guide starts with the six steps, then shows all three ways with working code, and the traps we hit when testing them on Python 3.14: a script that imports fine in your terminal but not under cron, and a scheduler that drifts.
On this page
- How to write a cron job in Python
- Reading the five time fields
- Which way should you pick?
- Way 1: a crontab line
- The virtualenv trap, measured
- Way 2: write the crontab from Python
- Way 3: a scheduler inside your program
- The schedule library drifts
- Running it in Docker
- FAQ
How to write a cron job in Python
These steps work on any Linux server or Mac with cron. The example runs digest.py every day at 9 am,
server time.
-
Write the script and use absolute paths inside it. Cron starts your job in your home directory, so a relative path such as
open("data.csv")points to the wrong place.python# /srv/app/digest.py from datetime import datetime from pathlib import Path HERE = Path(__file__).resolve().parent (HERE / "digest-last-run.txt").write_text(f"{datetime.now():%Y-%m-%d %H:%M:%S}\n") print("digest done") -
Find the full path of the Python that has your packages. Inside a virtualenv it is
.venv/bin/pythonin your project. Print it once so you can copy it:bashcd /srv/app && realpath .venv/bin/python -
Run the script with that path, exactly as cron will. If this fails, cron will fail too:
bash/srv/app/.venv/bin/python /srv/app/digest.py -
Open your crontab with
crontab -e. It opens in your editor; each line is one job. -
Add one line: the schedule, then the command, with output sent to a log.
0 9 * * * cd /srv/app && /srv/app/.venv/bin/python digest.py >> /srv/app/digest.log 2>&1 -
Save, then check.
crontab -llists the job. After 9 am, look indigest.logfordigest done. If it is empty, the first error line in that log tells you why.
That is a complete Python cron job. The rest of this page explains each choice in that line, and the other two ways to schedule Python.
Reading the five time fields
A crontab line starts with five fields, in this order: minute, hour, day of month, month and day of week.
A * means “every”. Common schedules:
| Schedule | Runs |
|---|---|
0 9 * * * |
Every day at 9:00 |
*/15 * * * * |
Every 15 minutes (at :00, :15, :30, :45) |
0 * * * * |
At the start of every hour |
30 2 * * 1-5 |
2:30 on weekdays |
0 0 1 * * |
Midnight on the 1st of every month |
Two details catch people out. A step only counts inside its own field, so */35 in the minute field runs
at :00 and :35, not every 35 minutes. And a % in the command is turned into a newline by cron, so a
command such as date +%F must be written date +\%F. Both rules are in the
crontab(5) manual.
Which way should you pick?
| Your situation | Use |
|---|---|
| A script that runs, finishes and exits, on a Linux server | Way 1: a crontab line |
| An installer or tool that must add cron entries for users | Way 2: python-crontab |
| A long-running app that should run small jobs on a timer | Way 3: an in-process scheduler |
If you are not sure, use Way 1. The operating system starts your script, so nothing has to stay running.
Way 1: a crontab line
Run crontab -e and add one line. This runs digest.py every day at 9 am, server time:
0 9 * * * cd /srv/app && /srv/app/.venv/bin/python digest.py >> /var/log/digest.log 2>&1
Three details in that line prevent most failures. It calls the virtualenv’s own Python by its full
path, it changes to the project folder first, and it sends output and errors to a log file. Make sure the
user that owns the crontab can write to that log path; /var/log often needs root, so a log inside your
project folder is the easy choice.
The virtualenv trap, measured
Cron does not load your shell, so it does not see an activated virtualenv. We reproduced cron’s empty
environment with env -i PATH=/usr/bin:/bin and ran a script whose package was installed only in the
virtualenv:
| Command run under a cron-like environment | Result |
|---|---|
python3 /home/app/job.py |
ModuleNotFoundError: No module named 'schedule' |
/home/app/.venv/bin/python /home/app/job.py |
Ran |
cd /tmp; /home/app/.venv/bin/python job.py |
can't open file '/tmp/job.py' |
So call the virtualenv’s interpreter directly, not python3 and not source activate. And use absolute
paths, or cd first, because cron starts your job in your home directory. Python’s
venv documentation confirms that running the venv’s
interpreter directly is enough; activation is optional.
Environment variables such as DATABASE_URL are missing for the same reason. Load them inside the script,
for example from a .env file. Our cron troubleshooting checklist covers
the other causes.
Way 2: write the crontab from Python
The python-crontab package edits crontab files from code. It is useful when your program installs its
own schedule, for example a CLI tool’s setup command:
from crontab import CronTab
cron = CronTab(user=True)
job = cron.new(
command="/srv/app/.venv/bin/python /srv/app/digest.py >> /var/log/digest.log 2>&1",
comment="digest",
)
job.setall("30 3 * * *")
cron.write()
Tested with python-crontab 3.4.0, it renders this line, with a comment so you can find and remove the job later:
30 3 * * * /srv/app/.venv/bin/python /srv/app/digest.py >> /var/log/digest.log 2>&1 # digest
Nothing is saved until cron.write() runs. It is still a normal cron job, so the virtualenv and timezone
rules above apply. Note 30 3: on a UTC server that is 9 am in India, as our
cron timezone guide explains.
Way 3: a scheduler inside your program
If your app already runs all the time, a library can call functions on a timer, with no crontab at all. The two common choices:
- schedule: tiny and readable,
schedule.every().day.at("09:00").do(job). No cron expressions, and no persistence. - APScheduler: cron expressions, timezones and optional job stores. Our APScheduler guide covers its restart and duplicate-run traps.
For Django or Flask apps that already use Celery, Celery beat is the usual fit.
Every in-process scheduler shares one rule: it runs once per copy of your program. Run the app on two servers, or with four Gunicorn workers that each start the scheduler, and each job runs two or four times.
The schedule library drifts
We scheduled schedule.every(5).seconds.do(job) with a job that takes 2 seconds, and ran it for 40
seconds with schedule 1.2.2.
| Run | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| Started at (seconds) | 5.0 | 12.0 | 19.1 | 26.1 | 33.1 | 40.1 |
The gap was 7 seconds, not 5: the library counts the next interval from when the job finished. In 40 seconds that is 6 runs instead of 8. For “every few minutes, roughly” that is fine. For “exactly on the hour”, use a cron expression instead.

Running it in Docker
A standard Python image has no cron daemon. Writing a crontab inside the container does nothing unless you install cron and run it as the main process, which also hides your environment variables from the job.
The simpler options: run the script from the host’s cron with docker run, use an in-process scheduler in
a container that stays up, or use your platform’s scheduled job feature.
Python cron jobs on Voroa
On Voroa, a Cron Job builds your repository and runs a command such as python digest.py on a
schedule, read in India Standard Time. Your environment variables are already set, so the virtualenv and
.env traps above do not apply.
The command runs inside a service that stays running, so a slow run can overlap the next, and runs are skipped while the service is stopped. See scheduling a task on Voroa and the Django deploy guide. Long-running consumers go on the background workers page.
FAQ
How do I create a cron job for a Python script?
Run crontab -e, add a line with the schedule and the full path to your Python and script, for example
0 9 * * * /full/path/to/.venv/bin/python /full/path/to/script.py >> /full/path/to/script.log 2>&1, and
save. crontab -l confirms it is installed.
Why does my Python script work in the terminal but not in cron?
Usually the wrong interpreter. Cron uses the system python3, which cannot see packages in your
virtualenv. Call the venv’s Python by its full path.
Do I need to activate the virtualenv in cron?
No. Running .venv/bin/python directly uses the virtualenv’s packages.
Do I need a shebang line or chmod +x?
Not if the crontab line starts with the interpreter, as in the examples here. You only need both if the
line calls the script on its own, such as /srv/app/digest.py.
Can Python run cron jobs without crontab? Yes, with an in-process library such as schedule or APScheduler, as long as your program keeps running.
How do I see the output of a Python cron job?
Redirect it: >> /srv/app/job.log 2>&1. Without that, cron mails the output to the crontab’s owner, or it
is lost if no mail is set up.
Tested on Python 3.14 with schedule 1.2.2 and python-crontab 3.4.0 on 7 November 2026; steps and crontab rules reviewed 14 November 2026. Found something out of date? Write to support@getvoroa.com.
Updated November 14, 2026