Scheduling Python Scripts

In the last lesson you learned Working with PDFs. So now you can build reports and read documents with code. But here is the next problem. You wrote a nice script that pulls data, cleans it up, and saves a report. Now you have to run it yourself every morning. That gets old fast. And the day you forget, the report is late. This lesson shows you how to make a script run on its own, on a schedule, so you never have to remember.

🤔 Why Schedule a Script?

Think about the things you do over and over. Back up a folder every night. Send a summary email every Monday. Check a website price every hour. Doing these by hand wastes your time. And worse, you forget.

Scheduling means telling the computer “run this code at this time, again and again” so it happens without you. You write the task once. The schedule takes care of the rest.

There are two situations, and they need different tools:

  • You have a program that is already running, and you want a function inside it to fire every few minutes. The schedule library is perfect here.
  • You want a script to run even when no program of yours is open, even after a restart. For that you hand the job to the operating system: cron on Linux and Mac, Task Scheduler on Windows.

We will cover both.

⏰ The schedule Library

schedule is a small third-party library. It does not run anything by itself. Instead, you tell it what to run and how often. Then you keep a loop running that asks it “is it time yet?” over and over.

Since it is third-party, install it first with pip:

Terminal window
pip install schedule

Here is the smallest example. It runs a function every few seconds so you can see it work without waiting around.

import schedule
import time
def job():
print("Running the job... done!")
# Tell schedule to run job() every 3 seconds
schedule.every(3).seconds.do(job)
# Keep checking forever
while True:
schedule.run_pending()
time.sleep(1)

Let’s read it line by line. The loop at the bottom is the part people miss.

  • schedule.every(3).seconds.do(job) reads almost like English. It says “every 3 seconds, do job”. Notice you pass job without the parentheses. You are handing over the function itself, not calling it now.
  • schedule.run_pending() is the worker. Each time you call it, it looks at the clock and runs any task that is due. If nothing is due, it does nothing.
  • time.sleep(1) makes the loop wait one second before checking again. Without it, the loop would spin as fast as the computer can go and waste the processor.

So the loop is the heartbeat. It wakes up once a second, asks schedule “anything due?”, and goes back to sleep.

Output

Running the job... done!
Running the job... done!
Running the job... done!

Note

This program never stops on its own. The while True loop runs forever. To stop it, press Ctrl + C in the terminal.

📅 Reading the Schedule Syntax

The nice thing about schedule is how readable it is. You build the rule by chaining words together. Here are the common ones.

This snippet shows several different timing rules side by side:

import schedule
def job():
print("doing the task")
schedule.every(10).minutes.do(job) # every 10 minutes
schedule.every().hour.do(job) # once an hour
schedule.every().day.at("09:30").do(job) # every day at 9:30 AM
schedule.every().monday.do(job) # every Monday
schedule.every().wednesday.at("18:00").do(job) # Wednesdays at 6 PM

A few things to notice:

  • When the number is 1, you can drop it. schedule.every().hour means every one hour. schedule.every(2).hours means every two hours, so the word becomes plural.
  • .at("09:30") uses a 24-hour clock as a string. So 6 PM is "18:00", not "6:00".
  • You can mix a day and a time together, like every().wednesday.at("18:00").
What you want The code
Every 5 minutes schedule.every(5).minutes.do(job)
Once an hour schedule.every().hour.do(job)
Every day at a set time schedule.every().day.at("08:00").do(job)
One weekday only schedule.every().friday.do(job)

🧪 A More Real Example

Let’s make it feel real. Say Alex wants a small health check that pings a website every minute and writes the result to a log file with a timestamp. This is the kind of task you would actually leave running.

This script defines the check, schedules it once a minute, and keeps the loop alive:

import schedule
import time
from datetime import datetime
def check_site():
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Pretend we checked the site and it responded OK
status = "OK"
line = f"{now} - site status: {status}\n"
with open("health_log.txt", "a") as f:
f.write(line)
print(f"Checked at {now} -> {status}")
# Run the check once every minute
schedule.every(1).minutes.do(check_site)
print("Health checker started. Press Ctrl + C to stop.")
while True:
schedule.run_pending()
time.sleep(1)

Here is what each part is doing:

  • datetime.now().strftime(...) builds a readable timestamp like 2026-06-12 09:30:00. We define this before we use it so the message and the log line both carry the same time.
  • We open the log file in "a" mode, which means append. Each run adds a new line instead of erasing the old ones.
  • The print gives you live feedback in the terminal while the file keeps the permanent record.

Because this depends on the current time, your timestamps will differ. Here is the sort of output you would see after it runs for a couple of minutes:

Output

Health checker started. Press Ctrl + C to stop.
Checked at 2026-06-12 09:30:00 -> OK
Checked at 2026-06-12 09:31:00 -> OK
Checked at 2026-06-12 09:32:00 -> OK

Note

This is example output. The exact times and the real site status depend on when you run it and what the site returns. So your lines will not match these character for character. That is normal for anything that reads the clock or the network.

🖥️ When the Program Must Be Closed: cron and Task Scheduler

Here is the catch with schedule. It only works while your Python program stays open. The moment you close the terminal or the computer restarts, the loop dies and nothing runs. That is fine for a script you watch over. But it is not enough for “run my backup every night at 2 AM whether or not I am awake.”

For that, you hand the job to the operating system. It will start your script for you, at the time you set, even with nothing of yours open.

Caution

Do not mix the two for the same task. If the OS already starts your script on a schedule, the script itself does not need the schedule loop. It just does its work once and exits. The OS handles the “when”.

cron on Linux and Mac

cron is a built-in scheduler on Linux and Mac. You give it a line that says when to run and what to run. You edit its list of jobs with the command crontab -e.

A cron line has five time fields followed by the command. This entry runs a script every day at 2:00 AM:

Terminal window
0 2 * * * /usr/bin/python3 /home/alex/scripts/backup.py

Reading the five fields left to right: minute, hour, day of month, month, day of week. A * means “every”. So 0 2 * * * means minute 0, hour 2, every day of the month, every month, every day of the week, which is 2:00 AM daily. Always use the full path to both Python and your script, because cron does not know about your shortcuts.

Task Scheduler on Windows

On Windows the built-in tool is Task Scheduler. It is a clickable app, so there is no special syntax to memorize. The steps are:

Step What you do
Open it Search “Task Scheduler” in the Start menu
Create Click “Create Basic Task” and give it a name
Trigger Pick when it runs, like Daily at a set time
Action Choose “Start a program”, point it at python.exe, and pass your script path as the argument

Both tools do the same job. They start your script at the right time without you being there.

⚠️ Common Mistakes

A few traps catch almost everyone the first time:

  • Calling the function instead of passing it. do(job()) runs job right away and hands schedule the result, which is wrong. Pass the name only: do(job).
# ❌ Avoid: job() runs once now and returns None; schedule then raises
# TypeError because None is not callable
schedule.every(5).minutes.do(job())
# ✅ Good: pass the function itself, schedule calls it later
schedule.every(5).minutes.do(job)
  • Forgetting the loop. Without while True with run_pending() inside, schedule never gets a chance to check the clock. Nothing fires.
  • A loop with no sleep. If you drop time.sleep(1), the loop spins as fast as it can and pushes the processor to full use for no reason.
  • Using a relative path with cron or Task Scheduler. These tools start in a different folder than you expect. Always give the full path to the script and to the files it touches.

✅ Best Practices

Small habits that keep scheduled jobs out of trouble:

  • Keep the scheduled function short and let it finish quickly, so the next run is never blocked.
  • Write a log line every time the job runs, with a timestamp. When something goes wrong at 2 AM, the log is how you find out.
  • Wrap risky work in try / except inside the job, so one bad run does not crash the whole loop.
  • Test the script by running it once by hand first. Only schedule it after you know it works on its own.
  • For “run even when nothing is open”, use cron or Task Scheduler. Use schedule only when a program of yours is already running.

🧩 What You’ve Learned

✅ Scheduling means telling the computer to run code at set times, again and again, without you.

✅ The schedule library lets you write readable rules like every().day.at("09:30").do(job), but it only runs while your program stays open.

✅ A schedule program needs a while True loop with run_pending() and a small sleep to keep checking the clock.

✅ To run a script when no program is open, use cron on Linux and Mac, or Task Scheduler on Windows.

✅ Always use full paths and write a log line so you can see what happened.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What does schedule.run_pending() actually do?

    Why: run_pending() looks at the current time and runs any task that is due; you call it repeatedly inside a loop.

  2. 2

    Why do you write do(job) and not do(job()) when scheduling?

    Why: Passing job() calls it immediately and hands schedule the return value; you must pass the function name so schedule can call it at the right time.

  3. 3

    Your script must run every night at 2 AM even when no program of yours is open. What should you use?

    Why: The schedule loop only runs while your program is open; the operating system's scheduler runs your script even when nothing of yours is running.

  4. 4

    What does the time.sleep(1) inside the scheduling loop prevent?

    Why: Without a short sleep, the loop checks the clock as fast as possible and pushes the processor to full use for no reason.

🚀 What’s Next?

You can now make your scripts run on their own, which is a big step toward real automation. Next we move into working with numbers and data at scale, starting with the library that powers most of Python’s data world.

Introduction to NumPy

Share & Connect