Practical guide · verified against the real thing
How to schedule Python scripts (cron, and the honest case against sleep loops)
In one line: cron for real schedules, the environment gotchas that break scheduled scripts, and why your script's own while-loop is usually the wrong tool.
Sooner or later every useful script earns a schedule: the report that should run at 6:30, the poller that should check every fifteen minutes. The reliable answers live outside Python — in the operating system's scheduler — and the unreliable answer is the one most people write first. Start with the right one.
cron: the workhorse (Linux and macOS)
Open your user's crontab with crontab -e and add a line: five time fields, then the command.
# every day at 06:30
30 6 * * * /usr/bin/python3 /home/you/jobs/report.py >> /home/you/jobs/report.log 2>&1
# every 15 minutes
*/15 * * * * /usr/bin/python3 /home/you/jobs/poll.py
The fields are minute, hour, day-of-month, month, day-of-week; * means "every", */15 means "every fifteenth". Three gotchas break most first attempts, so embed the fixes: use the absolute Python path (which python3 tells you yours) because cron's PATH is not your shell's; use absolute script paths because cron runs from a different working directory than your terminal; and redirect output (>> log 2>&1) because a scheduled script that fails silently is invisible — the log line turns every failure into evidence. One subtlety worth knowing before it costs an evening: % is special in crontab commands (it starts a stdin block), so any command that needs a literal percent sign must escape it as \%. On Windows, the Task Scheduler is the equivalent — point a task at python.exe with the script as its argument, on your trigger schedule.
The sleep-loop trap
The instinctive alternative — a while True loop with time.sleep(900) — has three failure modes that only show up in week three: the script drifts (sleep time plus work time per cycle), a crash takes the schedule down with it (nothing restarts the process), and a deploy silently orphans the old copy. The OS scheduler solves all three: it starts fresh, logs, and survives reboots. Reserve in-process repetition for genuinely in-process jobs — and if you need one, the standard library's sched module shows the correct pattern (a scheduler queue, not a sleep guess):
import sched
import time
scheduler = sched.scheduler(time.time, time.sleep)
def job(n):
print("job", n, "ran at", time.strftime("%H:%M:%S"))
if n < 3:
scheduler.enter(2, priority=1, action=job, argument=(n + 1,))
scheduler.enter(1, priority=1, action=job, argument=(1,))
scheduler.run()
print("all runs complete")
Drop the guard on n and the job reschedules itself forever. For heavier needs — persistent jobs, missed-run handling, distributed workers — dedicated schedulers exist (APScheduler in-process, Celery for task queues); adopt them when a cron line genuinely stops being enough, not before. And whatever runs on a schedule needs what every unattended script needs: the error handling of a proper API ladder and a storage layer that survives crashes — SQLite over CSV, every time.
Sources
Next