Code of the Day
IntermediateScheduling and Configuration

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.

WorkflowIntermediate8 min read
Recommended first
By the end of this lesson you will be able to:
  • 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:

Python — editable, runs in your browser

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 as 0 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

SituationTool
Simple recurring tasks in a long-running processschedule
Jobs that must survive process restartsAPScheduler with a job store
Jobs triggered by system events or run as standalone scriptscron
Complex dependencies between jobsAirflow, 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.

Finished reading? Mark it complete to track your progress.

On this page