CronHub

Build cron expressions visually — with a live schedule preview.

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?

SituationBest choice
Standalone script on a scheduleSystem cron
App must create/remove jobs dynamicallypython-crontab
Scheduling inside a long-running serviceAPScheduler
Sub-minute intervalsAPScheduler
Shared hosting without crontab accessAPScheduler (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

  1. Using bare python in the crontab. Cron's PATH probably doesn't include it, and it may resolve to the wrong version. Always use the absolute interpreter path.
  2. Trying to source venv/bin/activate in a cron line. Unnecessary and fragile — call venv/bin/python directly.
  3. Relative file paths in the script. Anchor to Path(__file__).resolve().parent.
  4. No output redirection. A crashing script with no log is invisible. Always append >> logfile 2>&1.
  5. 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