Scheduling Python Scripts: cron, Task Scheduler, and Email Alerts
Part 3 of the Python for Automation track. Last updated: September 2026.
A script you run by hand is a tool. A script that runs itself every morning at 7 AM and emails you only when something needs attention is an employee. This post covers the three ways to schedule Python scripts — the schedule library, cron on Linux/Mac, and Task Scheduler on Windows — plus the two things that make unattended scripts trustworthy: logging and email alerts.
Option 1: the schedule library (in-Python scheduling)
For schedules that live inside your program — "check prices every 30 minutes while this runs" — the schedule library (pip install schedule) is the friendliest option:
# pip install schedule
import schedule
import time
def check_prices():
print("Checking prices...")
schedule.every(30).minutes.do(check_prices)
schedule.every().day.at("07:00").do(check_prices)
schedule.every().monday.at("09:00").do(check_prices)
while True:
schedule.run_pending() # run any jobs that are due
time.sleep(60) # then sleep a minute
The API reads like English. The tradeoff: your script must stay running — great for a small server or a Raspberry Pi, not for a laptop that sleeps.
Option 2: cron (Linux and Mac)
cron is the system scheduler — it runs commands on a timetable even when you are logged out. Edit your timetable with crontab -e. Each line has five time fields, then the command:
# ┌─ minute (0-59) # │ ┌─ hour (0-23) # │ │ ┌─ day of month (1-31) # │ │ │ ┌─ month (1-12) # │ │ │ │ ┌─ day of week (0-6, Sunday=0) # │ │ │ │ │ # 0 7 * * * /usr/bin/python3 /home/you/deal_tracker.py >> /home/you/tracker.log 2>&1
That line runs deal_tracker.py every day at 7:00 AM and appends all output to a log file. Common patterns:
- */15 * * * * — every 15 minutes
- 0 9 * * 1 — every Monday at 9:00 AM
- 30 18 * * * — every day at 6:30 PM
Three cron gotchas that bite everyone once: use absolute paths (cron's PATH is minimal — /usr/bin/python3, not python3), redirect output to a log (>> tracker.log 2>&1) or failures vanish silently, and remember the script runs with your user's environment — test it with the full paths first.
Option 3: Windows Task Scheduler
On Windows, the equivalent is Task Scheduler. The quick command-line version:
# Run daily at 7:00 AM as the current user schtasks /create /tn "DealTracker" /tr "C:\Python312\python.exe C:\scripts\deal_tracker.py" /sc daily /st 07:00
Or use the GUI: Task Scheduler → Create Basic Task → trigger "Daily" → action "Start a program" → point it at python.exe with the script path as the argument. Either way, check "Run whether user is logged on or not" if the machine stays on.
Email alerts with smtplib
A scheduled script should be silent on success, loud on problems. smtplib sends email straight from the standard library. For Gmail, create an app password (Google Account → Security → 2-Step Verification → App passwords) and store it in an environment variable — never in the code:
import os
import smtplib
from email.message import EmailMessage
def send_alert(subject: str, body: str, to: str) -> None:
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = os.environ["ALERT_FROM"] # you@gmail.com
msg["To"] = to
msg.set_content(body)
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
smtp.login(os.environ["ALERT_FROM"], os.environ["ALERT_APP_PASSWORD"])
smtp.send_message(msg)
# send_alert("Price drop!", "Wireless Mouse is now $24.99 (was $29.99).", "you@gmail.com")
Logging: make unattended scripts debuggable
When a script runs at 7 AM with nobody watching, print() is useless. The logging module writes timestamped records to a file you can inspect later:
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("tracker.log"), # to file...
logging.StreamHandler(), # ...and to console
],
)
log = logging.getLogger(__name__)
log.info("Starting price check")
log.warning("example.com was slow (8s), continuing")
log.error("Failed to parse price for 'USB-C Hub'")
# 2026-09-30 07:00:01 [INFO] Starting price check
Log INFO for milestones ("checked 12 products"), WARNING for recoverable oddities, ERROR for failures. When something breaks at 7 AM, the log tells you exactly where.
Putting it together: the reliable job template
Every scheduled script I write follows this shape — work wrapped in try/except, email on failure, logging throughout:
import logging
import traceback
log = logging.getLogger(__name__)
def main():
log.info("Job started")
# ... do the work (scrape, compare, organize) ...
log.info("Job finished OK")
if __name__ == "__main__":
try:
main()
except Exception:
log.error("Job crashed:\n" + traceback.format_exc())
send_alert("Automation FAILED: deal tracker",
traceback.format_exc(), "you@gmail.com")
raise
The script emails you the traceback when it crashes — so a broken scraper wakes you up instead of silently producing nothing for a month.
Key takeaways
- schedule = in-Python timetables ("every 30 minutes"); the script must stay running.
- cron = the Linux/Mac system scheduler; five time fields, absolute paths, always log to a file.
- Task Scheduler / schtasks = the Windows equivalent.
- Unattended scripts need logging (INFO/WARNING/ERROR to a file) and email alerts on failure via smtplib.
- Store email credentials in environment variables (Gmail app password), never in code.
Next in this series: Build a Daily Deal Tracker: Scrape, Compare, and Email Yourself.
Comments
Post a Comment