Chapter 22 Quiz: Scheduling and Task Automation

Answer all questions before checking the answer key at the end.


Section A: Multiple Choice (1 point each)

Question 1. What is the primary purpose of the while True: schedule.run_pending(); time.sleep(60) pattern?

a) To run a job exactly once and then exit b) To create an infinite loop that periodically checks whether any scheduled jobs are ready to run and executes them c) To prevent the scheduler from running more than one job per minute d) To ensure that jobs run with exactly 60 seconds between each execution


Question 2. Which schedule library call correctly schedules a job to run every Monday at 7:45 AM?

a) schedule.every(7).days.at("07:45").do(job) b) schedule.every().week.at("Monday 07:45").do(job) c) schedule.every().monday.at("07:45").do(job) d) schedule.weekly("Monday", "07:45").do(job)


Question 3. What does the cron expression 0 8 * * 1-5 mean?

a) Every 8 minutes, Monday through Friday b) 8:00 AM on the 1st through 5th of every month c) Every hour between 8 AM and 5 PM, Monday through Friday d) 8:00 AM every weekday (Monday through Friday)


Question 4. You have a scheduled job that calls an external API and saves data to a CSV file. The external API goes down one Monday morning and the job throws a ConnectionError. What should happen?

a) The scheduler should crash so you know something went wrong b) The job should raise the exception so the scheduler catches it and skips all future runs of this job c) The job's exception handler should log the error and return normally so the scheduler continues running other jobs and future runs of this job d) Python automatically retries failed jobs three times before giving up


Question 5. What is the advantage of APScheduler's BackgroundScheduler over the schedule library's while True loop approach?

a) BackgroundScheduler is faster and executes jobs in less time b) BackgroundScheduler runs jobs in background threads, allowing your main program to continue executing other code without being blocked by the scheduler loop c) BackgroundScheduler does not require any third-party packages d) BackgroundScheduler automatically retries all failed jobs


Question 6. In Windows Task Scheduler, what does the "Start when available" (or "Run task as soon as possible after a scheduled start is missed") setting do?

a) Starts the task immediately if the machine is turned on before the scheduled time b) Runs the task right away if the scheduled time passed while the machine was off or busy c) Prevents the task from running if the machine is already running other tasks d) Allows the task to start regardless of whether the user is logged in


Question 7. You want to schedule a Python script to run every day at 6 AM using macOS/Linux cron. Which crontab entry is correct?

a) 6:00 * * * * python3 /path/to/script.py b) 0 6 * * * /path/to/venv/bin/python /path/to/script.py c) every day at 6:00 run python3 /path/to/script.py d) 00:06:00 daily /path/to/venv/bin/python /path/to/script.py


Question 8. What is the purpose of a "heartbeat file" pattern in scheduled automation?

a) To track how long each job takes to execute b) To regularly update a file with the current timestamp so an external process can verify the scheduler is still running c) To count the number of successful job runs since the scheduler started d) To pause the scheduler between jobs so it doesn't use too much CPU


Question 9. Which APScheduler trigger is most appropriate for scheduling a job to run on the first day of every month?

a) IntervalTrigger(days=30) b) DateTrigger(run_date="2024-01-01") c) CronTrigger(day=1, hour=0, minute=0) d) CronTrigger(month="*/1", day=1)


Question 10. What is logging.handlers.TimedRotatingFileHandler used for in a scheduler context?

a) Sending log messages to multiple destinations simultaneously b) Filtering log messages by level before writing to a file c) Automatically archiving old log files and creating new ones at regular intervals (daily, weekly, etc.) so log files don't grow indefinitely d) Rotating the execution order of jobs based on their runtime


Section B: True or False (1 point each)

Question 11. The schedule library can run scheduled jobs even when the Python process is not running.

Question 12. If a scheduled job raises an unhandled exception, the schedule library's default behavior is to remove that job from the schedule permanently.

Question 13. Windows Task Scheduler requires the user to be logged in for scheduled tasks to run.

Question 14. In APScheduler, setting coalesce=True means that if a job missed multiple scheduled runs (due to downtime), it will catch up by running once rather than running multiple times.

Question 15. The .env file loaded by python-dotenv is automatically available to processes launched by Windows Task Scheduler or cron without any additional code.


Section C: Code Analysis (2 points each)

Question 16. This scheduling code has a serious problem. Identify it and explain the consequences:

import schedule
import time

def pull_api_data():
    import requests
    response = requests.get("https://api.example.com/data", timeout=120)
    data = response.json()
    save_to_database(data)

schedule.every(5).minutes.do(pull_api_data)

while True:
    schedule.run_pending()
    time.sleep(60)

Question 17. Review this job decorator implementation. Is it correct? What would happen if the decorated job raises an exception?

def safe_job(func):
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            print(f"Job failed: {e}")
    return wrapper

@safe_job
def generate_report():
    load_data()
    create_excel()
    send_email()

schedule.every().monday.at("08:00").do(generate_report)

Question 18. This APScheduler code is missing something critical for a production deployment. What is it?

from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger

scheduler = BackgroundScheduler()
scheduler.add_job(
    func=generate_report,
    trigger=CronTrigger(day_of_week="mon", hour=8, minute=0),
    id="monday_report",
)
scheduler.start()

while True:
    time.sleep(60)

Question 19. What is wrong with this crontab entry?

45 7 * * MON python monday_report.py

Identify at least two problems.


Section D: Short Answer (3 points each)

Question 20. Priya's Monday report pipeline takes about 25 seconds to run. The schedule library loop sleeps for 60 seconds between checks (time.sleep(60)). Explain why this is still correct behavior — why the 60-second sleep interval does not cause the job to be delayed by 60 seconds past its scheduled time.


Question 21. A colleague suggests that instead of scheduling the Monday report to run at 7:45 AM, they should schedule it to run at 12:00 AM Monday (midnight) "to be safe — that way it's definitely done before the meeting." List at least three reasons why this is a poor practice and explain the better approach.


Question 22. Explain the concept of "defensive exit codes" in the context of Windows Task Scheduler. What is an exit code? Why does it matter that a Python script exits with code 0 on success and a non-zero code on failure? What does sys.exit(1) do differently from letting the script end naturally?


Question 23. A scheduled job that checks for overdue invoices ran successfully at 8:00 AM. It found 3 overdue invoices and sent an alert email. At 8:05 AM, the same job ran again unexpectedly (a configuration error ran it twice). Describe two approaches to make this kind of "double run" harmless: one using a state file, and one using idempotent job design.


Answer Key

Section A: 1. b 2. c 3. d 4. c 5. b 6. b 7. b 8. b 9. c 10. c

Section B: 11. False — the schedule library (and any pure-Python scheduler) requires a running Python process. If the process is not running, no jobs execute. 12. False — schedule's default behavior when a job raises an exception is to leave the job in the schedule and continue. The exception propagates unless caught, which may crash the scheduler loop — but it does not remove the job. 13. False — Windows Task Scheduler can be configured to run tasks whether the user is logged in or not (the "Run whether user is logged on or not" setting). 14. True — coalesce=True is the setting that prevents a job from running multiple times to catch up after downtime; it runs once instead. 15. False — .env files are not automatically inherited by processes launched by Task Scheduler or cron. The script must explicitly call load_dotenv() with the full path to the .env file.

Section C:

  1. Problem: The timeout=120 on the API call means the function can block for up to 2 minutes. Since schedule runs jobs synchronously, a 120-second blocking job will prevent the scheduler loop from checking for other jobs during that time. If the API consistently times out, the 5-minute schedule becomes a "run as fast as possible with a 2-minute minimum gap" schedule. Consequence: All other jobs pile up behind it. If the scheduler is supposed to run 5-minute health checks AND a job that blocks for 2 minutes, the health checks will be delayed. Fix: use a shorter timeout (15-30 seconds) for API calls that should be fast, add retry logic that doesn't block indefinitely, or use APScheduler with thread pool execution so long jobs don't block others.

  2. Is it correct? Mostly yes — the exception is caught and logged, and the scheduler continues. However, it has two weaknesses: (1) it uses print() instead of logging — print output may not appear in automated contexts (cron, Task Scheduler), while logging to a file will; (2) it wraps with a plain wrapper function without @functools.wraps(func), which destroys the original function's __name__ and docstring — schedule's job list will show wrapper instead of generate_report. Both are minor but the logging issue is consequential in production.

  3. Missing: Graceful shutdown handling. The code starts the scheduler but has no way to stop it cleanly. If the process receives a SIGTERM (e.g., from a system shutdown or kill), the while True loop will be interrupted abruptly, potentially interrupting a running job mid-execution (leaving partial output files, incomplete database writes, etc.). Fix: wrap the while True in a try/except (KeyboardInterrupt, SystemExit) and call scheduler.shutdown() in the cleanup code.

  4. Problems: (1) MON uses three-letter weekday abbreviation — in cron, weekday values should be 0-7 (numbers) or Mon (some cron implementations accept abbreviations, but this is not universal — use 1 for Monday to be safe); (2) python is a bare command without a full path — cron runs with a minimal PATH that may not include the Python installation directory. Use the full path: /usr/bin/python3 or /home/user/.venv/bin/python; (3) monday_report.py is a relative path — cron's working directory is typically the user's home directory, not the script's directory. Use the full absolute path. (4) There is no output redirection — any print statements or errors will be sent to email (if configured) or lost silently.

Section D:

  1. Explanation: The time.sleep(60) determines how often the scheduler checks whether jobs are due, not how late they run. At 7:44:00, the loop wakes up, checks schedule.run_pending(), finds no jobs due, and sleeps 60 seconds. At 7:45:00, the loop wakes again, checks run_pending(), finds the Monday report is due (scheduled for 7:45), and runs it immediately. The job starts approximately on time — within 60 seconds of its scheduled time, which is acceptable for a business report. If you needed tighter scheduling (sub-minute precision), you'd use time.sleep(1) instead. The 60-second sleep is a tradeoff: less CPU usage at the cost of up to 60 seconds of scheduling imprecision.

  2. Problems with midnight scheduling: (1) Midnight Monday is actually Sunday night — scheduling it for "Monday" at midnight in most contexts means it runs at the very start of Monday, which may be before the weekend data has been compiled (if the sales team uploads Sunday night, the data might not arrive until 11 PM Sunday, making a midnight Monday run too early); (2) If the report fails at midnight, no one is awake to diagnose and fix it before the 9 AM meeting; (3) Log files and monitoring dashboards are harder to read when tasks run at unusual hours; (4) If the report data needs to be as recent as possible, midnight gives data that's as fresh as it was when the Sunday upload happened — a 7:45 AM run has the same data plus 7+ more hours of weekend activity if any exists. Better approach: Schedule as close to the meeting time as practical while leaving enough buffer to fix failures — 7:45 AM for a 9:00 AM meeting is a reasonable compromise.

  3. Exit codes: An exit code is an integer the operating system records when a process ends. 0 conventionally means success; any non-zero value means failure. Windows Task Scheduler records the exit code in its task history. If a task exits with 0, the history shows "Completed" (green). If it exits with a non-zero code like 1, the history shows "Failed" or "Completed (with errors)" (highlighted in red). This allows IT monitoring tools to detect failures without reading log files. sys.exit(1) terminates the Python interpreter with exit code 1 and triggers any registered atexit handlers. A script that ends naturally (falls off the end of __main__) exits with code 0 — so a script that silently swallows all exceptions and returns normally will always appear successful to Task Scheduler, even when it failed.

  4. Approach 1 — State file: After sending the alert, write a file last_alert_sent_YYYY-MM-DD.txt. Before sending the next alert, check whether this file exists and was written within the last N hours. If it was, skip the alert. This prevents duplicate alerts within a time window. Approach 2 — Idempotent design: Design the job so running it twice produces the same result as running it once. Rather than "send an alert if overdue invoices exist," the job could "send an alert if overdue invoices exist AND no alert has been sent in the last 8 hours." The idempotency check is built into the business logic rather than the scheduling infrastructure. For invoice monitoring, a practical version: log each alert to a database or CSV with a timestamp; before sending, query whether an alert for the same invoice was sent today; if yes, skip. Running the job twice with this design sends the same invoices' alerts only once.


Total points: 10 (Section A) + 5 (Section B) + 8 (Section C) + 12 (Section D) = 35 points

Suggested passing score: 28/35 (80%)