Learning Goals
3 minBy the end of this lesson you can:
- Read and write cron schedule syntax (the five fields).
- Register a job with
crontab(Linux/macOS) or Task Scheduler (Windows). - Write scripts that run correctly under a scheduler (absolute paths, env, logging).
- Choose between
scheduleand the OS scheduler for a given need.
Warm-Up · Who Keeps Your Script Alive?
5 minLast lesson's schedule loop only fires while that Python process runs. Close the terminal, reboot the machine, hit a crash — and your 8am report silently stops. Who's watching?
The OS already has a rock-solid, always-running scheduler: cron on Linux/macOS, Task Scheduler on Windows. You register "run this command at this time" once, and the OS launches a fresh run of your script on schedule — surviving reboots and crashes. The shift in thinking: your script becomes a one-shot again (do the work, exit), and the OS handles the repetition.
New Concept · cron & Task Scheduler
14 mincron syntax: five fields
┌─ minute (0-59) │ ┌─ hour (0-23) │ │ ┌─ day of month (1-31) │ │ │ ┌─ month (1-12) │ │ │ │ ┌─ day of week (0-6, Sun=0) │ │ │ │ │ * * * * * command-to-run 0 8 * * * every day at 08:00 */10 * * * * every 10 minutes 0 2 * * 0 Sundays at 02:00 30 9 1 * * 09:30 on the 1st of each month
* = "every," */n = "every n," lists (1,15) and ranges (9-17) work too. The site crontab.guru is invaluable for reading/writing these.
Adding a cron job (Linux/macOS)
crontab -e # opens your crontab in an editor # add a line — note the ABSOLUTE paths: 0 8 * * * /usr/bin/python3 /home/aisha/report.py >> /home/aisha/report.log 2>&1 crontab -l # list your jobs
- Use the absolute path to python and your script — cron has a minimal environment, not your shell's.
>> logfile 2>&1appends both stdout and stderr to a log — essential, since cron's output otherwise vanishes (or emails root).
Task Scheduler (Windows)
# via PowerShell, register a daily 8am task:
$action = New-ScheduledTaskAction -Execute "python" `
-Argument "C:\Users\aisha\report.py"
$trigger = New-ScheduledTaskTrigger -Daily -At 8am
Register-ScheduledTask -TaskName "DailyReport" `
-Action $action -Trigger $triggerOr use the Task Scheduler GUI: Create Task → Trigger (Daily 8am) → Action (Start a program: python, arguments: your script's full path), set "Start in" to the script's folder.
Writing scheduler-friendly scripts
- Working directory differs — relative paths break. Anchor with
Path(__file__).resolve().parent(Lesson 8). - Minimal environment — your
.env/ PATH may be absent. Use absolute python, load.envexplicitly, set vars in the cron line if needed. - No terminal —
printgoes nowhere useful. Log to a file (Lesson 14) with absolute paths. - Runs as a different user — permissions and home folder differ from your interactive session.
Worked Example · A Cron-Ready Script
12 minGoal: take the report generator and make it bullet-proof under a scheduler — absolute paths, explicit env loading, file logging, and a clear exit code.
#!/usr/bin/env python3 """report_cron.py — designed to be launched by cron / Task Scheduler.""" import sys, logging from pathlib import Path from logging.handlers import RotatingFileHandler from datetime import datetime from dotenv import load_dotenv # 1) anchor everything to THIS file's location, not the cwd HERE = Path(__file__).resolve().parent # 2) load env explicitly from a known path load_dotenv(HERE / ".env") # 3) log to a FILE with an absolute path (cron has no terminal) log = logging.getLogger("report") log.setLevel(logging.INFO) handler = RotatingFileHandler(HERE / "logs" / "report.log", maxBytes=1_000_000, backupCount=5) handler.setFormatter(logging.Formatter( "%(asctime)s %(levelname)s %(message)s")) (HERE / "logs").mkdir(exist_ok=True) log.addHandler(handler) def main() -> int: log.info("=== run started %s ===", datetime.now().isoformat()) try: data_file = HERE / "data" / "today.csv" # absolute, not relative if not data_file.exists(): log.error("no data file: %s", data_file) return 1 # …generate the report (Lesson 22)… log.info("report generated successfully") return 0 except Exception: log.exception("run failed") return 1 if __name__ == "__main__": sys.exit(main()) # exit code: 0 ok, non-zero failure
# crontab line that runs it daily at 8am, logging output too: 0 8 * * * /usr/bin/python3 /home/aisha/app/report_cron.py >> /home/aisha/app/cron.log 2>&1 # logs/report.log then accumulates: 2026-05-28 08:00:01 INFO === run started 2026-05-28T08:00:01 === 2026-05-28 08:00:03 INFO report generated successfully
Read the code
Every scheduler gotcha is pre-empted: HERE anchors all paths to the script's own folder (so the cwd doesn't matter), load_dotenv(HERE / ".env") loads secrets from a known location, logging goes to a rotating file (the terminal is gone), and sys.exit(main()) returns a proper exit code the scheduler can act on. The script does its work once and exits — the OS handles the "every day." This is the difference between a script that works when you run it and one that works at 8am while you sleep.
Try It Yourself
13 minTranslate these to English: 0 0 * * *, */15 9-17 * * 1-5, 0 0 1 1 *. Then write cron lines for "every Sunday at 23:00" and "every 5 minutes during business hours on weekdays." (Check with crontab.guru.)
Write a tiny script that appends the current timestamp to a log file (absolute path!), then schedule it to run every minute via cron or Task Scheduler. Watch the log grow, then remove the job.
Hint
# tick.py from pathlib import Path from datetime import datetime HERE = Path(__file__).resolve().parent with open(HERE / "tick.log", "a", encoding="utf-8") as f: f.write(datetime.now().isoformat() + "\n") # crontab line: * * * * * /usr/bin/python3 /full/path/tick.py
Deliberately schedule a script that uses a relative path (e.g. open("data.csv")) and watch it fail silently under cron. Then fix it with Path(__file__).parent and confirm it works. Document what went wrong — this is the #1 real-world cron bug.
Mini-Challenge · The Install Helper
8 minWrite a Python helper that generates the correct scheduler command for the current OS: on Linux/macOS it prints the crontab line (with absolute python + script paths), on Windows it prints the PowerShell Register-ScheduledTask command. Use sys.executable and Path(__file__).resolve() so the paths are always right.
Show a sample solution
import sys from pathlib import Path def install_command(script: str, time_hhmm: str = "08:00") -> str: py = sys.executable # the exact python running this script_path = str(Path(script).resolve()) hh, mm = time_hhmm.split(":") if sys.platform.startswith("win"): return (f'$a = New-ScheduledTaskAction -Execute "{py}" ' f'-Argument "{script_path}"\n' f'$t = New-ScheduledTaskTrigger -Daily -At {time_hhmm}\n' f'Register-ScheduledTask -TaskName "MyJob" -Action $a -Trigger $t') else: log = str(Path(script_path).with_suffix(".log")) return f'{mm} {hh} * * * "{py}" "{script_path}" >> "{log}" 2>&1' print("Add this to your scheduler:\n") print(install_command("report_cron.py", "08:00"))
Non-negotiables: OS detection, absolute python (sys.executable) + script paths, correct cron/Task-Scheduler syntax.
Recap
3 minFor jobs that must survive reboots and crashes, register them with the OS scheduler — cron (five fields: min hour day month weekday) on Linux/macOS, Task Scheduler on Windows. Your script becomes a one-shot the OS launches on schedule. Make it scheduler-friendly: absolute paths (anchor to Path(__file__).parent), explicit .env loading, file logging (no terminal), and a proper sys.exit code. Use schedule for in-process/dev scheduling; use the OS scheduler for production reliability. The relative-path-under-cron failure is the classic bug — pre-empt it.
Vocabulary Card
- cron
- The Unix time-based job scheduler; jobs live in a crontab.
- crontab
- The file/list of cron jobs; edit with
crontab -e. - Task Scheduler
- Windows' built-in scheduler for running programs at set times.
- cron environment
- The minimal env cron jobs run in — why absolute paths matter.
Homework
4 minTake one automation you've built and make it fully "cron-ready": absolute paths, explicit env loading, file logging, exit codes. Schedule it on your machine (every few minutes for testing), confirm it runs and logs correctly when launched by the scheduler (not by you), then change it to a sensible real cadence. Write down the exact scheduler line/command you used and one gotcha you hit.
Sample · what a complete submission shows
Script: report_cron.py — paths anchored to Path(__file__).parent,
load_dotenv(HERE/".env"), RotatingFileHandler to HERE/logs,
sys.exit(main()) returning 0/1.
Scheduler line (Linux):
*/5 * * * * /usr/bin/python3 /home/me/app/report_cron.py >> /home/me/app/cron.log 2>&1
Gotcha hit: first attempt used open("data/today.csv") (relative)
and produced an empty log + FileNotFoundError, because cron's
working directory was / not my project. Fixed with HERE / "data" /
"today.csv". Then switched cadence from */5 to '0 8 * * *'.Non-negotiables: a cron-ready script, an actual scheduled run that logs to a file, the exact line used, and a documented gotcha.