Timing, Retrying and Scheduling
About 24 minutes
Why this matters
Automation scripts live in a messy world: networks drop, servers are busy, and jobs must run at set times. Four small ideas cover most of it: timing how long something takes, waiting (sleep), retrying when something fails, and scheduling work to repeat.
The idea
import time
start = time.perf_counter() # a precise clock for measuring durations
do_something()
print(time.perf_counter() - start, "seconds")
time.sleep(2) # pause the program for 2 seconds
Retrying with backoff. A flaky operation often works if you simply try again, a little later each time:
def retry(func, attempts, delay):
for attempt in range(attempts):
try:
return func()
except Exception:
if attempt == attempts - 1:
raise # out of attempts: let the error out
time.sleep(delay)
delay *= 2 # wait longer each time ("exponential backoff")
Testing code that waits. A test that really sleeps for 30 seconds is useless. Instead, pass the clock and the sleep function in as parameters, with the real ones as defaults:
def retry(func, attempts, delay, sleep=time.sleep): ...
In normal use nothing changes. In a check you hand in a fake sleep that just records the delays, so the test runs instantly and can check them. This is called dependency injection, and it is how you test anything involving time.
Schedules. Arithmetic with datetime and timedelta computes when jobs should run:
from datetime import datetime, timedelta
first = datetime(2026, 1, 1, 9, 0)
[first + timedelta(minutes=30 * i) for i in range(3)] # 9:00, 9:30, 10:00
For jobs that must run when your program is not running, use the operating system’s scheduler instead (cron on Linux and macOS, Task Scheduler on Windows) and let it start your script.
Try it
Time a loop that adds up range(1_000_000) with perf_counter. Then write a function that raises an error the first two times it is called and succeeds on the third, and call it through the retry above.
Your turn
Every function that waits takes a sleep (and the stopwatch a clock) so it can be checked quickly.
Stopwatch(clock=time.perf_counter): a class withstart()andstop();stop()returns the seconds sincestart().retry(func, attempts, delay, sleep=time.sleep): callfunc()until it succeeds and return its result. After a failure waitdelayseconds, doubling the delay each time. If every attempt fails, raise the last error. Do not sleep after the final failure.next_runs(start, every_minutes, count): a list ofcountdatetimes, beginning atstart, spacedevery_minutesapart.run_every(task, interval, times, sleep=time.sleep): calltask()timestimes, waitingintervalseconds between calls (not after the last), and return the list of results.
Recap
perf_counter()measures durations;sleep()pauses.- Retry with a growing delay, and give up after a fixed number of attempts, re-raising the error.
- Pass
sleepandclockin as parameters so tests do not have to wait. datetime + timedeltacomputes schedules; usecronor Task Scheduler for jobs outside your program.
Common mistakes
- Retrying forever. Always cap the attempts.
- Catching an error and hiding it after the last attempt; re-raise it so the caller knows.
- Hard-wiring
time.sleepinto a function, which makes it slow and hard to check.