Skip to main content

schedium logo

schedium

A lightweight, composable, in-process, pure-python job scheduler.

Tests Docs PyPI version Python versions License Ruff numpydoc mypy


Why schedium?

Most Python schedulers either require a background thread / daemon or force you into a rigid cron syntax. schedium takes a different approach:

  • No threads, no processes — jobs run inline when you call run_pending(). They can be run in threads or asynchronously using helpers.
  • Composable triggers — build complex schedules by combining simple primitives with & (AND) and | (OR). See composing triggers doc
  • Automatic deduplication — calling run_pending() multiple times within the same time bucket is safe; jobs run at most once per bucket. See deduplication doc
  • Zero dependencies — pure Python, nothing outside the standard library.
  • Fully typed — first-class type annotations and mypy-checked.
  • Supports all currently maintained Python versions: 3.10, 3.11, 3.12, 3.13, and 3.14.

link to the documentation

Installation

pip install schedium

Quick start

import time
from schedium import Every, Job, Scheduler, Weekly

sched = Scheduler()

def hello():
    print("hello!")

# Every 5 minutes
sched.append(Job(hello, Every(unit="minute", interval=5), name="5-min"))

# Every Monday at 09:30
sched.append(Job(hello, Weekly("monday", at="09:30"), name="weekly"))

while True:
    sched.run_pending()
    time.sleep(1)

Threading (optional)

schedium runs jobs inline by default. If you want multi-threading, use the helpers in schedium.threading:

  • ThreadedJobsScheduler: runs each due job on a worker thread (thread pool).
  • QueuedJobsScheduler: keeps the scheduler in your thread and enqueues due jobs for worker threads.
  • SchedulerThread: runs the scheduler loop itself in a dedicated thread.
from schedium import Every, Job, Scheduler
from schedium.threading import SchedulerThread, ThreadedJobsScheduler

sched = Scheduler()
sched.append(Job(lambda: print("tick"), Every(unit="second", interval=1)))

threaded = ThreadedJobsScheduler(sched, max_workers=8)
runner = SchedulerThread(threaded, interval=1.0)
runner.start()

# ... later
runner.stop()
runner.join()
threaded.shutdown()

Async (optional)

For asyncio applications, use schedium.asyncio.AsyncScheduler. It is used the same way as Scheduler, but runs due jobs on the event loop — async def jobs are awaited directly, plain sync jobs run in an executor so they never block the loop.

import asyncio
from schedium import Every, Job
from schedium.asyncio import AsyncScheduler

async_sched = AsyncScheduler()
async_sched.append(Job(lambda: print("tick"), Every(unit="second", interval=1)))

async def main():
    while True:
        await async_sched.run_pending()
        await asyncio.sleep(1)

asyncio.run(main())

Composing triggers

Triggers are the building blocks of schedules. Combine them freely:

from schedium import Every, On, Between

# Every minute, but only on weekdays between 9 AM and 5 PM
trigger = (
    Every(unit="minute", interval=1)
    & On(unit="weekdays")
    & Between(unit="hour_of_day", start=9, end=17)
)
from schedium import Every, On

# Every hour, at minute 12 OR minute 55
trigger = (
    Every(unit="hour", interval=1)
    & (On(unit="minute_of_hour", value=12) | On(unit="minute_of_hour", value=55))
)

Available triggers

Trigger Role Example
Every(unit, interval) Epoch-aligned cadence Every(unit="minute", interval=5)
Tick(granularity) Always matches; sets the dedup bucket Tick("day")
On(unit, value) Equality constraint On(unit="hour_of_day", value=8)
Between(unit, start, end) Range constraint (inclusive) Between(unit="hour_of_day", start=9, end=17)
AtDateTime(run_date) One-shot at a specific datetime AtDateTime(datetime(2026, 3, 1, 12, 0))
BetweenDateTime(start, end) Datetime window constraint BetweenDateTime(start_date=..., end_date=...)
Daily(at=...) Convenience: daily (optionally at a time) Daily(at="09:30")
Weekly(day, at=...) Convenience: weekly on a weekday Weekly("mon", at="09:30")
trigger_a & trigger_b AND combinator All conditions must match
trigger_a | trigger_b OR combinator Either condition can match

Deduplication

schedium automatically deduplicates job runs. Calling run_pending() repeatedly within the same time bucket will only execute the job once:

from datetime import datetime
from schedium import JobDidNotRun, Every, Job, Scheduler

sched = Scheduler()
sched.append(Job(lambda: print("tick"), Every(unit="minute", interval=1)))

# First call at 10:05 → runs the job
sched.run_pending(now=datetime(2026, 2, 4, 10, 5, 0))

# Second call at 10:05 → already ran for this bucket
result = sched.run_pending(now=datetime(2026, 2, 4, 10, 5, 0))
assert result[0] is JobDidNotRun

# Next minute → runs again
sched.run_pending(now=datetime(2026, 2, 4, 10, 6, 0))

Inspecting next run times

from datetime import datetime
from schedium import Every, Job, Scheduler

sched = Scheduler()
sched.append(Job(lambda: None, Every(unit="minute", interval=5)))

next_run = sched.time_of_next_run(after=datetime(2026, 2, 4, 10, 3, 0))
print(next_run)  # datetime(2026, 2, 4, 10, 5, 0)

Timezone handling

For predictable behavior, use UTC-aware datetimes:

import time
from datetime import datetime, timezone
from schedium import Every, Job, Scheduler

sched = Scheduler()
sched.append(Job(lambda: print("tick"), Every(unit="minute", interval=1)))

while True:
    sched.run_pending(now=datetime.now(timezone.utc))
    time.sleep(1)

Local timezones work too (via zoneinfo), but be aware of DST transitions — see the docs for details.

Documentation

Full documentation is built with Sphinx and hosted alongside the project:

  • Guides: Scheduler usage, job creation, trigger composition
  • Concepts: Granularity, trigger tokens & deduplication, window time
  • API reference: Every class and function documented with numpydoc

Build locally:

pip install -e . --group docs
sphinx-build -b html docs docs/_build/html

Development

Setup

git clone https://github.com/MarcBresson/schedium.git
cd schedium
python -m venv .venv && source .venv/bin/activate
pip install -e . --group dev --group test --group docs
pre-commit install

Run tests

pytest

Linting & formatting

The project uses Ruff for linting and formatting, mypy for type checking, and numpydoc for docstring validation — all enforced via pre-commit:

pre-commit run --all-files

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch (git switch -c feature/my-feature)
  3. Ensure all tests pass (pytest) and pre-commit hooks are clean
  4. Open a pull request

License

schedium is licensed under the Apache License 2.0.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

schedium-1.0.0.tar.gz (45.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

schedium-1.0.0-py3-none-any.whl (47.8 kB view details)

Uploaded Python 3

File details

Details for the file schedium-1.0.0.tar.gz.

File metadata

  • Download URL: schedium-1.0.0.tar.gz
  • Upload date:
  • Size: 45.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for schedium-1.0.0.tar.gz
Algorithm Hash digest
SHA256 a811b7bd5fa4210388490fa181b13e9112b5686ee3396bcbbaa6da3d5eea1772
MD5 c73282d42c26b5e623596aa334e75aa7
BLAKE2b-256 693df7f88786a2ce10a4d22961cd6a048726ce0082c42e23a836b0f0a5fcbfc9

See more details on using hashes here.

Provenance

The following attestation bundles were made for schedium-1.0.0.tar.gz:

Publisher: publish.yml on MarcBresson/schedium

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file schedium-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: schedium-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 47.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for schedium-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6affbdbd5fbd62f195daaa1e441e334f681c32b80369d3008af6847eccb5df96
MD5 5c1d1e46d7a436f8e5407f1f1b944a57
BLAKE2b-256 3039a4c610cff4ad41eb27840ec93ff8713c5c037cf4eba994a25e70910bb801

See more details on using hashes here.

Provenance

The following attestation bundles were made for schedium-1.0.0-py3-none-any.whl:

Publisher: publish.yml on MarcBresson/schedium

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

1 file

0.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page