recurrence
A zero-dependency, modern Python replacement for schedule — with async support, thread safety, monthly scheduling, a pluggable Clock protocol, structured error handling, and concurrent job execution.
Installation
pip install recurrence
Quickstart
import recurrence
def greet():
print("Hello!")
# Run every 10 seconds
recurrence.every(10).seconds.do(greet)
# Run every Monday at 9am
recurrence.every().monday.at("09:00").do(greet)
# Run every 2 hours
recurrence.every(2).hours.do(greet)
# Main loop
import time
while True:
recurrence.run_pending()
time.sleep(1)
Key Features
Async Support
Schedule coroutines directly — no boilerplate needed:
import asyncio
import recurrence
async def fetch_data():
await asyncio.sleep(0)
print("fetched")
recurrence.every(30).seconds.do(fetch_data)
# In an async context, use run_pending_async:
async def main():
while True:
await recurrence.default_scheduler.run_pending_async()
await asyncio.sleep(1)
asyncio.run(main())
Thread-Safe
Scheduler uses an internal threading.RLock so jobs can be added or cancelled from any thread without data races.
Monthly Scheduling
# Run on the 1st of every month at midnight (approximate — uses calendar arithmetic)
recurrence.every().month.at("00:00").do(my_monthly_report)
Clock Protocol
Inject a custom clock for deterministic testing:
import datetime
from recurrence import Scheduler
class FakeClock:
def __init__(self, dt):
self._dt = dt
def now(self, tz=None):
return self._dt
clock = FakeClock(datetime.datetime(2024, 6, 1, 12, 0, 0))
scheduler = Scheduler(clock=clock)
scheduler.every(10).seconds.do(lambda: print("ran"))
Error Handling
Provide a per-scheduler or per-job error handler instead of crashing the loop:
def handle_error(job, exc):
print(f"Job {job} failed: {exc}")
scheduler = recurrence.Scheduler(on_error=handle_error)
scheduler.every(5).seconds.do(risky_function)
# Or per-job:
job = recurrence.Job(on_error=handle_error)
scheduler.every(5).seconds.do(risky_function)
Concurrent Execution
Run jobs in a thread pool so slow jobs don't block each other:
scheduler = recurrence.Scheduler(max_workers=4)
scheduler.every(1).seconds.do(slow_job)
scheduler.every(1).seconds.do(another_slow_job)
scheduler.run_pending() # both run concurrently
Migration from schedule
One-line import change:
# Before
import schedule
# After
import recurrence as schedule
Everything else stays the same. recurrence is a strict superset of the schedule API.
API Reference
Module-level functions (default scheduler)
| Function | Description |
|---|---|
every(interval=1) |
Create a new job on the default scheduler |
run_pending() |
Run all jobs that are due |
run_all(delay_seconds=0) |
Run all jobs immediately |
get_jobs(tag=None) |
Return all jobs, optionally filtered by tag |
clear(tag=None) |
Remove all jobs, or only those with the given tag |
cancel_job(job) |
Remove a specific job |
next_run() |
Datetime of the next scheduled run |
idle_seconds() |
Seconds until the next job runs |
repeat(job, *args, **kwargs) |
Decorator to schedule a function |
Job fluent API
every(10).seconds.do(fn)
every(5).minutes.do(fn)
every(2).hours.at(":30").do(fn)
every().day.at("10:30").do(fn)
every().monday.at("09:00").do(fn)
every().month.at("00:00").do(fn)
# Random interval
every(5).to(10).seconds.do(fn)
# Deadline
every(1).hours.until("18:00").do(fn)
# Tags
every(10).seconds.do(fn).tag("my-tag")
# Cancel from within the job
def my_job():
return recurrence.CancelJob # removes itself after running
Scheduler class
s = recurrence.Scheduler(
timezone="America/New_York", # str, zoneinfo.ZoneInfo, or pytz tz
clock=my_clock, # any object with .now(tz) -> datetime
on_error=my_handler, # callable(job, exc)
max_workers=4, # enable concurrent execution
)
s.every(10).seconds.do(fn)
s.run_pending()
await s.run_pending_async()
s.shutdown() # clean up thread pool
License
MIT
Metadata
Release files for recurrence 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| recurrence-0.1.0.tar.gz | 21.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| recurrence-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 33.6 kB
Release files / recurrence-0.1.0.tar.gz
| Download URL | recurrence-0.1.0.tar.gz |
|---|---|
| Size | 21.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6e2fa7b9817aa0fb0de0082af3dbdecc373208fa6dc89d9824b8030dfb0694a4
|
|
BLAKE2b-256 checksum How to use checksums |
2468cfb0101bf1052dffaa985e553d4916f938d768edcb8dcda0de7815a0e28f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 13, 2026.
Transparency logRelease files / recurrence-0.1.0-py3-none-any.whl
| Download URL | recurrence-0.1.0-py3-none-any.whl |
|---|---|
| Size | 11.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
695f90104227b29977d1feef5bb381f852829ae666c67ddf3d419a0ae1c558bf
|
|
BLAKE2b-256 checksum How to use checksums |
b0fb9cb1cd651f8ba7610bc40cb1deec51bcf6acbf9ccde1924bac2f023ac8ff
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 13, 2026.
Transparency log