Scheduling logic
A scheduler's job boils down to one repeated question: "is it time to run yet?" A loop wakes up periodically, asks that question, and fires the job when the answer is yes. The hard part is writing that decision so it's correct and testable.
The single most important trick: pass the time in as an argument — don't read the clock inside the function.
````
def should_run(last_run, interval, now): # GOOD — now is passed in
return now - last_run >= interval
If the function calls time.time() or datetime.now() internally, you can't test it — the answer changes every second and you can't reproduce a bug. Pass now in and the function is deterministic: the same inputs always give the same answer, so you can test "what happens at exactly 07:00" without waiting until 7am.
Common scheduling shapes:
- Interval: fire if
now - last_run >= interval_seconds. Alast_runof 0 means "never run" — so it's immediately due. - Cron-style at-an-hour: fire if it's the target hour
andwe haven't already run today (now.date() != last_run_date). The "already ran today" guard is what stops it firing 60 times during the target hour. - Interval math: a frequent cron idiom is
minute % interval == 0— fire on minute 0, 5, 10, … for a 5-minute interval.
You can also compute how long until the next run: next_due - now, which goes negative when you're overdue.
Syntax
import datetime
# Interval: due if enough time has passed (last_run=0 means never)
def should_run_interval(last_run_unix, interval_s, now_unix):
return now_unix - last_run_unix >= interval_s
# Cron-at-hour: right hour AND not already today
def should_run_at_hour(target_hour, last_run_date, now_dt):
return now_dt.hour == target_hour and now_dt.date() != last_run_date
# every-N-minutes idiom
# now_dt.minute % interval == 0Worked examples
Interval check — deterministic, time passed in
def should_run_interval(last_run_unix, interval_s, now_unix):
return now_unix - last_run_unix >= interval_s
print(should_run_interval(1000, 60, 1061)) # True (61s passed)
print(should_run_interval(1000, 60, 1060)) # True (exactly at boundary)
print(should_run_interval(1000, 60, 1059)) # False (1s too soon)
print(should_run_interval(0, 60, 1000)) # True (never run → due)
# Output:
# True
# True
# False
# TrueRun once a day at a target hour
import datetime
def should_run_at_hour(target_hour, last_run_date, now_dt):
if now_dt.hour != target_hour:
return False
if last_run_date is not None and now_dt.date() == last_run_date:
return False
return True
now = datetime.datetime(2026, 6, 14, 7, 30)
print(should_run_at_hour(7, datetime.date(2026, 6, 13), now)) # True (new day)
print(should_run_at_hour(7, datetime.date(2026, 6, 14), now)) # False (ran today)
print(should_run_at_hour(7, None, datetime.datetime(2026, 6, 14, 9, 0))) # False (wrong hour)
# Output:
# True
# False
# FalseTime until next run (negative = overdue) + every-N-minutes
def next_run_in(last_run_unix, interval_s, now_unix):
return (last_run_unix + interval_s) - now_unix
print(next_run_in(1000, 60, 1020)) # 40 (not due for 40s)
print(next_run_in(1000, 60, 1100)) # -40 (overdue by 40s)
# cron 'every 5 minutes' idiom
fires = [m for m in range(0, 16) if m % 5 == 0]
print(fires)
# Output:
# 40
# -40
# [0, 5, 10, 15]Common mistakes & gotchas
Reading the clock inside the function
Calling time.time() or datetime.now() inside the decision makes it untestable and non-reproducible — every run gives a different answer. Pass now in as a parameter; let only the outermost loop touch the real clock. This is the whole reason the examples are deterministic.
Missing the 'already ran today' guard
An hour-based check that only tests now.hour == target_hour will fire on *every* tick during that hour — potentially 60 times. You need the second condition (now.date() != last_run_date) to ensure it runs once per day, not once per minute for an hour.
> vs >= at the interval boundary
now - last_run > interval skips the run at the exact boundary (e.g. exactly 60s after a 60s interval), so the job drifts a tick later every cycle. Use >= so it fires *at* the boundary. One character decides whether a daily job slowly creeps off-schedule.
Why it matters
Every cron job, polling loop, and recurring task is built on "is it time yet?" logic. Writing that decision deterministically — with the time passed in, not read from the clock — is what lets you unit-test scheduling without waiting hours for the real moment, and it's why production schedulers can prove they'll fire exactly when they should.