Python schedulers
The schedule library lets you define recurring jobs with readable syntax inside a Python process — a practical alternative to cron for scripts you control directly.
- Define recurring jobs with schedule.every() using human-readable syntax
- Run a scheduler loop with schedule.run_pending() and time.sleep()
- Compare schedule to APScheduler for more complex scheduling needs
Cron is a system daemon — it runs jobs independently of your Python process, which is
exactly right for production servers. But sometimes you already have a long-running
Python process (a service, a bot, a data pipeline) and you want to schedule tasks
within it. The schedule library is the standard lightweight choice for this.
The schedule library
schedule provides a fluent API for defining jobs:
import schedule
import time
def send_report():
print("Sending report...")
def sync_data():
print("Syncing data...")
schedule.every().hour.do(send_report)
schedule.every().day.at("09:00").do(sync_data)
schedule.every(5).minutes.do(send_report)
schedule.every().monday.at("08:00").do(sync_data)The API reads like English. every().hour runs once per hour. every().day.at("09:00")
runs daily at 9am. every(5).minutes runs every five minutes.
The run loop
schedule does not run jobs automatically — you drive it with a loop:
while True:
schedule.run_pending()
time.sleep(1)run_pending() checks whether any scheduled job is due and runs it synchronously if so.
The time.sleep(1) keeps the loop from burning CPU. The granularity is approximately
one second — fine for most automation tasks.
schedule runs jobs in the same thread as the loop. If a job takes longer than its
interval, the next run is delayed. For jobs that might take a long time, run them in
a separate thread: schedule.every().hour.do(lambda: threading.Thread(target=my_job).start()).
Scheduling with a time string
The .at("HH:MM") format accepts 24-hour time:
schedule.every().day.at("00:00").do(midnight_job) # midnight
schedule.every().day.at("14:30").do(afternoon_job) # 2:30pm
schedule.every().wednesday.at("09:00").do(weekly_job)Try it
The runner below defines a job and simulates the scheduler loop without actually sleeping — it advances time manually to show what would run when:
You should see job_a running at seconds 2, 4, 6, 8 and job_b at second 5. The
exact timing depends on the runner, but the pattern is consistent.
APScheduler for more complex needs
schedule is deliberately minimal. When you need more, APScheduler
provides:
- Cron-syntax triggers:
CronTrigger(hour=9, minute=0)— same as0 9 * * * - Persistent job stores: survive a process restart by storing schedules in a database
- Timezone-aware scheduling: critical when your process runs in UTC but jobs should fire in local time
- Misfire policies: what to do if a job was missed because the process was down
For simple in-process scheduling, schedule is the right choice. Reach for APScheduler
when you need persistence, timezone handling, or cron-syntax triggers.
When to use each tool
| Situation | Tool |
|---|---|
| Simple recurring tasks in a long-running process | schedule |
| Jobs that must survive process restarts | APScheduler with a job store |
| Jobs triggered by system events or run as standalone scripts | cron |
| Complex dependencies between jobs | Airflow, Prefect, or similar |
Where to go next
Next: config files — before a scheduled job runs, it often needs to know where to write output, how verbose to be, and what parameters to use. Configuration files are how you separate those decisions from the code.
Cron concepts
Cron is the Unix scheduler — learn to read and write crontab expressions, understand common schedule patterns, and know where cron falls short.
Config files
Separating configuration from code lets you change a script's behaviour without editing source — understand the common formats and the layered override pattern that makes scripts flexible and safe.