Python Cron Job: 3 Ways to Schedule Scripts
The fastest way to create a Python cron job is one line in your crontab, pointing at the exact interpreter and the exact script:
*/15 * * * * /home/user/venv/bin/python /home/user/app/task.py >> /home/user/logs/task.log 2>&1
That runs task.py every 15 minutes using a virtual environment's Python and logs everything it prints. For most people scheduling a Python script, that's the whole answer.
But there are three genuinely different ways to run Python on a schedule, and picking the right one saves headaches later: system cron (simplest, most robust), the python-crontab library (manage crontabs from Python code), and APScheduler (scheduling inside a long-running Python app). Let's set up each one properly.
Method 1: System Cron (Recommended for Most Cases)
Step 1 — Find your real interpreter
The number-one cause of failed Python cron jobs is the interpreter. Cron doesn't know about your shell's PATH, your pyenv shims, or your activated venv. Get the absolute path:
which python3
# /usr/bin/python3
# or, for a virtual environment:
ls /home/user/venv/bin/python
Key insight: you never need to "activate" a venv in cron. Calling the venv's python binary directly gives you that environment's packages automatically.
Step 2 — Add the crontab entry
crontab -e
# Data sync every 15 minutes, using the project venv
*/15 * * * * /home/user/venv/bin/python /home/user/app/task.py >> /home/user/logs/task.log 2>&1
Everything is absolute: the interpreter, the script, the log. The 2>&1 sends Python tracebacks to the same log as normal output — without it, errors vanish.
Step 3 — Handle paths inside the script
Cron runs your script from your home directory, so relative file access breaks. Anchor paths to the script's own location:
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
config = BASE_DIR / "config.json"
output = BASE_DIR / "data" / "results.csv"
Step 4 — See output in real time
Python buffers stdout when it's not attached to a terminal, so print() lines may not appear in your log until the script exits. For long-running jobs, use unbuffered mode:
*/15 * * * * /home/user/venv/bin/python -u /home/user/app/task.py >> /home/user/logs/task.log 2>&1
Or better, use the logging module with a file handler and skip the issue entirely.
Method 2: python-crontab — Manage Cron From Python
The python-crontab library reads and writes real crontabs programmatically. It's the right tool when your application needs to install, list, or remove its own scheduled jobs — think a web app that lets users schedule reports.
pip install python-crontab
from crontab import CronTab
cron = CronTab(user=True) # current user's crontab
job = cron.new(
command='/home/user/venv/bin/python /home/user/app/task.py',
comment='app-data-sync',
)
job.minute.every(15)
cron.write() # installs the entry
List or remove jobs by the comment tag you set:
for job in cron.find_comment('app-data-sync'):
cron.remove(job)
cron.write()
Note what this library is and isn't: it edits crontabs — the actual scheduling is still done by the system cron daemon. Your Python process doesn't need to stay alive.
Method 3: APScheduler — Scheduling Inside a Python App
APScheduler (Advanced Python Scheduler) flips the model: the schedule lives inside a running Python process. That's what you want when scheduling is part of an application — a Flask/FastAPI app, a bot, a data service — rather than a standalone task.
pip install apscheduler
from apscheduler.schedulers.blocking import BlockingScheduler
scheduler = BlockingScheduler()
@scheduler.scheduled_job('cron', minute='*/15')
def sync_data():
print("Syncing…")
scheduler.start()
The 'cron' trigger accepts familiar cron-style fields (minute, hour, day_of_week…), and an 'interval' trigger handles "every N seconds" cases that system cron can't express.
The trade-off: the process must keep running. If it dies, so does your schedule — so in production you'd run it under systemd or a container orchestrator. Which raises a fair question: if you need a supervisor anyway, would plain system cron be simpler? Often, yes.
Which Method Should You Use?
| Situation | Best choice |
|---|---|
| Standalone script on a schedule | System cron |
| App must create/remove jobs dynamically | python-crontab |
| Scheduling inside a long-running service | APScheduler |
| Sub-minute intervals | APScheduler |
| Shared hosting without crontab access | APScheduler (or the host's control panel) |
A Production-Ready Script Template
Whichever scheduler launches it, a Python task script benefits from three habits baked in — proper logging, a top-level exception catch, and a meaningful exit code:
import logging, sys
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
logging.basicConfig(
filename=BASE_DIR / "task.log",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
def main():
logging.info("run started")
# ... your work here ...
logging.info("run finished")
if __name__ == "__main__":
try:
main()
except Exception:
logging.exception("run failed")
sys.exit(1)
The sys.exit(1) matters more than it looks: a non-zero exit code is what lets &&-chained monitoring pings, MAILTO alerts, and wrapper scripts distinguish success from failure.
Common Mistakes With Python Cron Jobs
- Using bare
pythonin the crontab. Cron's PATH probably doesn't include it, and it may resolve to the wrong version. Always use the absolute interpreter path. - Trying to
source venv/bin/activatein a cron line. Unnecessary and fragile — callvenv/bin/pythondirectly. - Relative file paths in the script. Anchor to
Path(__file__).resolve().parent. - No output redirection. A crashing script with no log is invisible. Always append
>> logfile 2>&1. - Overlapping runs. A 20-minute script on a 15-minute schedule stacks up. Wrap it:
flock -n /tmp/task.lock /home/user/venv/bin/python ….
Conclusion
For a Python cron job, start with the simplest tool that fits: a single crontab line calling your venv's interpreter with absolute paths and logged output. Reach for python-crontab when code needs to manage schedules, and for APScheduler when the scheduler belongs inside your application. Whichever route you take, the absolute-path rule and the logging rule are what separate jobs that quietly work for years from jobs that quietly fail.
New to the five-field schedule format? Our cron syntax guide covers it in depth, and the troubleshooting guide is there if your script runs in the terminal but not on the clock.
Frequently Asked Questions
How do I run a Python script every day with cron?
Add 0 6 * * * /usr/bin/python3 /home/user/script.py >> /home/user/logs/script.log 2>&1 to your crontab (via crontab -e) for a 6 a.m. daily run. Use the output of which python3 — or your venv's python — as the interpreter path.
Do I need to activate my virtualenv in a cron job?
No. Activation just tweaks your shell's PATH. Calling the venv's interpreter directly — /home/user/venv/bin/python script.py — runs with all of that environment's packages, no activation step needed.
Why does my Python cron job produce no output in the log?
Python buffers stdout when not attached to a terminal, so prints appear only when the buffer flushes or the process exits. Run with python -u for unbuffered output, or use the logging module with a file handler.
Is APScheduler better than cron for Python?
Neither is strictly better. System cron is more robust for standalone scripts because the OS supervises it. APScheduler wins when scheduling must live inside an always-running Python application, or when you need sub-minute intervals.
Need the expression itself? Build and test any cron schedule in our free visual generator — with live upcoming-run previews in Unix and Quartz formats.
Related guides
References
- APScheduler documentation — official user guide and trigger reference
- python-crontab on PyPI — installation and API documentation
- man7.org — crontab(5) manual page — the underlying cron format