153. Cron + Python Crontab + Schedule
Scheduling recurring tasks with cron and Python
153. Cron + Python Crontab + Schedule
He wants the monitor to run automatically every 5 minutes.
π€ Socratic question: Your phone sends you app notifications, your bank runs interest calculations overnight, your streaming service generates recommendations while you sleep. None of this has a human clicking βrun.β Who clicks run? Nobody. Thatβs what schedulers are for.
ποΈ History: Cron was written by Ken Thompson (co-inventor of Unix, C, Go, and UTF-8) in 1975. The name comes from the Greek Chronos, god of time. Its cron syntax hasnβt changed in 50 years. The exact same
* * * * *format that Thompson wrote in 1975 is what youβll use today. Your bank uses it. NASA uses it. And now, you will too.
Cron: The System Scheduler π
Cron runs commands on a schedule. Itβs been in Unix since 1975.
# Edit your crontab
crontab -e
# List your cron jobs
crontab -l
# Remove all cron jobs (be careful)
crontab -rCron jobs survive reboots. They run even when youβre not at the keyboard.
π‘ Real-world: Your bank calculates interest every night at midnight β thatβs a cron job. Twitter/X generates βtrending topicsβ every 15 minutes β cron job. Instagram sends you βyou have new activityβ emails β cron job. Most of the internet runs on 50-year-old scheduling code.
Cron Field Order πΊοΈ
The hardest part: getting the field order right.
ββββββββββββ minute (0β59)
β ββββββββββ hour (0β23)
β β ββββββββ day of month (1β31)
β β β ββββββ month (1β12)
β β β β ββββ day of week (0β6, Sun=0)
β β β β β
* * * * * commandCommon patterns:
*/5 * * * * every 5 minutes
0 9 * * * every day at 9:00 AM
0 9 * * 1-5 weekdays at 9:00 AM
0 0 1 * * first day of every monthπΊ YouTube search: βcron job explained in 5 minutesβ β every video uses the same mnemonic: βMinute Hour DayOfMonth Month DayOfWeekβ. Youβll never forget it.
Installing a Cron Job from Python π
import sys
from crontab import CronTab
cron = CronTab(user=True)
cron.remove_all(comment='ram_manager') # remove old jobs first
job = cron.new(
command=f'{sys.executable} /path/to/ram_manager.py monitor',
comment='ram_manager', # tag for easy removal later
)
job.minute.every(5) # sets */5 * * * *
cron.write()Critical: use sys.executable, not 'python3'. In a virtualenv, python3 may point to the system Python, not your venv. sys.executable always points to the exact interpreter currently running.
π War story: Developers have deployed cron jobs that ran with the wrong Python and silently failed for weeks because
python3pointed to the system Python and their dependencies werenβt installed there.sys.executableis the correct answer. Always.
In-Process Scheduling with schedule β±οΈ
For development and testing, you can run the scheduler inside Python:
import schedule, time
def check():
snapshot = take_snapshot()
if snapshot['percent'] > 80:
print(f"β οΈ RAM: {snapshot['percent']:.1f}%")
schedule.every(5).minutes.do(check)
while True:
schedule.run_pending()
time.sleep(1) # NEVER skip thisWithout time.sleep(1), the while loop spins at full CPU speed doing nothing. One line. Huge impact on battery and CPU temperature.
π€― Fun fact: A busy-wait loop (while True with no sleep) will peg your CPU at 100% and drain your laptop battery in under an hour. Pythonβs GIL doesnβt save you here. One
time.sleep(1)fixes it. The difference between 0% CPU and 100% CPU is one line.
Cron vs In-Process Scheduler βοΈ
| Cron | schedule | |
|---|---|---|
| Survives reboot? | Yes | No |
| Runs when script exits? | Yes | No |
| Granularity | 1 minute | 1 second |
| Good for | Production | Development, demos |
| Setup | crontab -e | Just Python |
For a real tool: use cron. For demos and testing: schedule is fine.
π‘ Real-world: AWS Lambda, Google Cloud Functions, and Azure Functions are all βserverless cronβ β you write a function and tell the cloud when to run it. The concept is identical to cron, just with auto-scaling and billing per execution instead of a single machine.
π‘ Fun fact: The crontab.guru website (crontab.guru) was created by a developer who got tired of debugging cron expressions. It processes over 1 million cron expression lookups per day β proof that even experienced developers confuse the field order constantly. Thereβs no shame in looking it up every time.
β οΈ Watch out: schedule.every(300).seconds schedules every 300 seconds (5 minutes). But schedule.every(5).minutes does the same thing and reads much better. The subtle danger is mixing up .seconds and .minutes β schedule.every(5).seconds runs every 5 seconds, not every 5 minutes. Always use the most human-readable unit so bugs are obvious at a glance.
π€ Think about it: Why does install_cron_job() call cron.remove_all(comment='ram_manager') before adding a new job? What happens if you call install_cron_job() twice without removing first? How is this pattern called? (Hint: itβs called βidempotencyβ β running an operation multiple times produces the same result as running it once. This is a critical property for deployment scripts, cron installers, and infrastructure as code.)
β° Aryan wants the RAM manager to run automatically every 5 minutes without him having to remember to start it. He confuses the cron field order, schedules a Python script using the wrong Python path, and doesnβt know how to list or remove existing cron jobs.
Learning objectives
- Know cron field order (minute hour day month weekday)
- Use sys.executable for the right Python path in cron jobs
- Tag cron jobs with comment= for easy management
- Use schedule library with time.sleep() for Python-only scheduling
- Know when to use cron vs schedule
Key concepts
- cron minute field β */5 = every 5 minutes
- sys.executable β path to current Python interpreter
- CronTab(user=True) β manage userβs crontab from Python
- schedule.every(N).minutes.do(fn) β Python scheduler
- time.sleep() in loop β avoid busy-wait
- cron.remove_all(comment=) β idempotent job management
Try it
Concept detail
Scheduling with Cron and Python
Cron syntax (field order matters!)
* * * * * command
β β β β βββ day of week (0-7, 0=Sun)
β β β βββββ month (1-12)
β β βββββββ day of month (1-31)
β βββββββββ hour (0-23)
βββββββββββ minute (0-59)*/5 * * * * # every 5 minutes
0 * * * * # every hour on the hour
0 9 * * 1-5 # 9am weekdays
0 0 1 * * # midnight on 1st of month
30 8 * * * # 8:30am every dayShell: manage cron jobs
crontab -l # list current jobs
crontab -e # edit (opens in $EDITOR)
crontab -r # remove all jobs (careful!)
# Add a job manually:
# */5 * * * * /usr/local/bin/python3 /home/aryan/ram_manager.pypython-crontab β manage cron from Python
from crontab import CronTab
import sys
cron = CronTab(user=True) # current user's crontab
# Add a job
job = cron.new(
command=f'{sys.executable} /path/to/ram_manager.py',
comment='ram-manager'
)
job.setall('*/5 * * * *') # every 5 minutes
cron.write()
# List, find, remove
for job in cron:
print(job)
cron.remove_all(comment='ram-manager')
cron.write()schedule library β pure Python scheduler
import schedule, time
def check_ram():
print('Checking RAM...')
schedule.every(5).minutes.do(check_ram)
schedule.every().hour.do(check_ram)
schedule.every().day.at('09:00').do(check_ram)
while True:
schedule.run_pending()
time.sleep(30) # check every 30sCron vs schedule library
| cron | schedule | |
|---|---|---|
| Survives reboot | yes | no (process must run) |
| Setup | one-time crontab edit | loop in Python |
| Good for | production servers | dev/simple scripts |
Solution
import sys
from crontab import CronTab
import schedule
import time
CRON_COMMENT = 'ram-manager'
def install_cron_job(script_path: str, interval_minutes: int = 5) -> None:
cron = CronTab(user=True)
# Remove existing ram-manager jobs first (idempotent)
cron.remove_all(comment=CRON_COMMENT)
# Use the current Python interpreter for the right venv
python = sys.executable
job = cron.new(command=f'{python} {script_path}', comment=CRON_COMMENT)
# Cron field order: minute hour day-of-month month day-of-week
job.setall(f'*/{interval_minutes} * * * *') # every N minutes
cron.write()
print(f'Installed: {job}')
def remove_cron_job() -> int:
cron = CronTab(user=True)
removed = cron.remove_all(comment=CRON_COMMENT)
cron.write()
return removed
def run_on_schedule(check_fn, interval_minutes: int = 5) -> None:
schedule.every(interval_minutes).minutes.do(check_fn)
print(f'Running every {interval_minutes} minutes. Ctrl+C to stop.')
while True:
schedule.run_pending()
time.sleep(30) # check every 30 seconds, not busy-wait
def list_ram_manager_jobs() -> list[str]:
cron = CronTab(user=True)
return [str(job) for job in cron if job.comment == CRON_COMMENT]Tests
import time
import pytest
import schedule as sched
def test_run_on_schedule_uses_sleep():
import inspect
src = inspect.getsource(run_on_schedule)
assert 'time.sleep' in src, 'Must sleep in the loop β no busy-wait'
def test_run_on_schedule_uses_minutes():
import inspect
src = inspect.getsource(run_on_schedule)
assert '.minutes' in src, 'Use schedule.every(N).minutes not .seconds'
def test_list_ram_manager_jobs_filters_by_comment():
import inspect
src = inspect.getsource(list_ram_manager_jobs)
assert 'comment' in src or 'CRON_COMMENT' in src, (
'Must filter by comment β do not return ALL cron jobs'
)
def test_install_cron_job_uses_sys_executable():
import inspect
src = inspect.getsource(install_cron_job)
assert 'sys.executable' in src, (
'Use sys.executable to get the right Python interpreter path'
)
def test_install_cron_job_sets_minute_interval():
import inspect
src = inspect.getsource(install_cron_job)
# The expression */N should appear in the minute field (first field)
assert '*/interval_minutes' in src or '*/{interval_minutes}' in src or 'setall' in src
def test_schedule_callback_called():
calls = []
sched.clear()
sched.every(1).seconds.do(lambda: calls.append(1))
time.sleep(1.1)
sched.run_pending()
assert len(calls) >= 1