Croner - Cron for Python
Croner is available for JavaScript/TypeScript · Rust · Python · .NET · C++
Parse and evaluate cron patterns, enumerate runs across time zones and DST, and schedule functions. No dependencies beyond the standard library. Python 3.9+.
- Evaluate patterns without scheduling anything: next and previous runs, matching, iteration.
- Schedule functions in threads, or as a decorator. Plain and
async deffunctions both work. - Time zone aware. Returns aware
datetimeobjects in anyzoneinfotime zone, a fixed UTC offset, or system local time, with well defined behavior when the clock changes for DST. - Extended syntax from the Open Cron Pattern Standard
(OCPS) 1.0 - 1.4: optional seconds and years,
L,W,#,+and?. - Over-run protection, error handling, pausing, max runs, intervals, start and stop dates, day offsets.
- Human readable descriptions of patterns.
- Fully typed (
py.typed).
Quick examples:
from itertools import islice
from croner import Cron
# Run a function at the fifth second of every minute
job = Cron("5 * * * * *", lambda: print("This will run every fifth second"))
# As a decorator, with over-run protection. The decorated name is the job.
@Cron("*/5 * * * * *", protect=True)
def every_five_seconds(job):
print("Every five seconds, never overlapping. Next run:", job.next_run())
every_five_seconds.pause()
# Run once, at a specific date and time
job = Cron("2027-01-23T00:00:00", lambda: print("Yay!"), timezone="Asia/Kolkata")
# The next 100 Sundays
next_sundays = Cron("0 0 * * SUN").next_runs(100)
# A job is an iterator over its upcoming runs
for run in islice(Cron("0 9 * * MON-FRI", timezone="Europe/Stockholm"), 5):
print(run.isoformat())
# Days left to Christmas Eve (a timedelta)
days_left = Cron("0 0 24 12 *").time_to_next().days
# Describe a pattern
print(Cron("0 12 * * MON-FRI").describe()) # At 12:00, on Monday, Tuesday, Wednesday, Thursday, and Friday.
Installation
pip install croner
On Windows, which has no system time zone database, also install tzdata to use the timezone option:
pip install croner[tzdata].
Documentation
Signature
Cron takes a pattern, an optional function, and keyword options:
# - pattern: Cron pattern, datetime (run once), or ISO 8601 string (run once)
# - fn: Optional function to schedule
# - options: Keyword options (optional)
job = Cron("* * * * * *", max_runs=1)
# If the function is omitted, it can be scheduled later
job.schedule(lambda job, context: None)
The function is called as fn(job, context), where context is the context option. A function that
takes fewer arguments gets fewer: fn(job) or fn(). Coroutine functions (async def) are run to
completion with asyncio.run() in the worker thread.
Each run happens in a thread of its own, so a slow run does not delay the schedule (use protect to
prevent overlapping runs). Scheduler threads are not daemon threads, so the interpreter keeps running
while a job is scheduled. Pass daemon=True to use daemon threads, letting the process exit while jobs
are still scheduled.
As a decorator, a job schedules the decorated function and takes its place. The decorated name then refers to the job, which can be paused, stopped and queried, and calling it calls the function:
@Cron("0 * * * *", daemon=True)
def hourly_report():
...
hourly_report.next_run() # the job
hourly_report() # runs the function now, in the calling thread
hourly_report.stop()
Status
job.next_run(start=None) # The next run (datetime), or None.
job.next_runs(10, start=None) # The next 10 runs.
job.previous_runs(10, reference=None) # The 10 previous scheduled runs, most recent first.
job.enumerate(start=None) # An iterator over the runs after start, with peek() and reset().
job.time_to_next(start=None) # Time to the next run (timedelta), or None.
job.ms_to_next(start=None) # The same in milliseconds (int).
job.current_run() # When the current (or last) run started.
job.previous_run() # When the previous run started.
job.runs_left() # Runs left, None for unlimited.
job.match(date) # Whether a datetime or ISO 8601 string matches the pattern.
job.is_running() # Scheduled, and not paused or stopped.
job.is_stopped() # Stopped with stop().
job.is_busy() # The function is running right now.
job.get_pattern() # The original pattern, or None for one-off jobs.
job.once # The date of a one-off job, or None. Also job.get_once().
job.describe() # Human readable description of the pattern.
iter(job) is the same as job.enumerate(), so a job can be used directly in a for loop or with
itertools. Without stop_at or max_runs the iterator is endless.
Control functions
job.trigger() # Run the function now, in the calling thread.
job.pause() # Pause.
job.resume() # Resume.
job.stop() # Stop permanently. Also removes a named job from scheduled_jobs.
Dates and time zones
Every method that takes a date accepts a datetime, a date or an ISO 8601 string.
- Aware datetimes, and strings with
Zor an offset, are exact instants. - Naive datetimes, dates, and strings without an offset are wall clock times in the job's time zone
(the
timezoneorutc_offsetoption), or in system local time if neither is set.
Returned datetimes are aware, in the job's time zone (zoneinfo.ZoneInfo), so they print in local time
and compare correctly as instants.
Note Python subtracts two aware datetimes that share a
tzinfoas wall clock times, ignoring DST. Convert to UTC first,a.astimezone(timezone.utc) - b.astimezone(timezone.utc), to get the real time between two runs across a DST change.
Options
| Key | Default | Type | Remarks |
|---|---|---|---|
| name | None | str | Keep a reference to the job in croner.scheduled_jobs. The reference is removed on stop(). |
| max_runs | None | int | Maximum number of runs, None for unlimited. |
| catch | False | bool | callable | Catch errors raised by the function. True silently ignores them, a callable is called as catch(error, job). |
| timezone | None | str | tzinfo | Time zone in Europe/Stockholm format, or a tzinfo (zoneinfo, dateutil, or pytz, which is read through zoneinfo). |
| utc_offset | None | int | timedelta | Schedule with a fixed UTC offset, in minutes or as a timedelta. This does not take DST into account, you probably want timezone. |
| start_at | None | datetime | str | Earliest run. Inclusive, a run exactly at start_at is included (except when interval is set). |
| stop_at | None | datetime | str | Stop before this date. Exclusive, a run exactly at stop_at is not included. |
| interval | 0 | int | timedelta | Minimum time between runs, in seconds or as a timedelta. |
| paused | False | bool | Start paused. |
| context | None | Any | Passed as the second argument to the function. |
| dom_and_dow | False | bool | Combine day-of-month and day-of-week with AND (True) or OR (False). |
| legacy_mode | - | bool | Deprecated: use dom_and_dow. Inverse of dom_and_dow. |
| daemon | False | bool | Run the scheduler and the function in daemon threads, so the process can exit while the job is scheduled. |
| unref | False | bool | Alias of daemon. |
| protect | False | bool | callable | Over-run protection: skip runs while a previous run is still going. A callable is called as protect(job) for each skipped run. |
| alternative_weekdays | False | bool | Quartz-style weekday numbering (1 = Sunday, 2 = Monday, ..., 7 = Saturday). |
| allow_past | False | bool | Let a one-off job dated in the past run immediately. Otherwise only one dated within the last second does. |
| day_offset | 0 | int | Shift every run by a number of days, e.g. -1 for the day before each match. All methods and start_at / stop_at refer to the shifted time. |
| mode | "auto" | str | How many fields the pattern has: "auto", "5-part", "6-part", "7-part", "5-or-6-parts" or "6-or-7-parts". |
| sloppy_ranges | False | bool | Accept non-standard steps, like /10, 5/5 or 30/30. By default only */step and min-max/step are allowed. |
Invalid patterns and options raise croner.CronError, a subclass of ValueError.
Pattern
Patterns are based on Vixie cron, with these extensions:
┌──────────────── (optional) second (0 - 59)
│ ┌────────────── minute (0 - 59)
│ │ ┌──────────── hour (0 - 23)
│ │ │ ┌────────── day of month (1 - 31)
│ │ │ │ ┌──────── month (1 - 12, JAN-DEC)
│ │ │ │ │ ┌────── day of week (0 - 6, SUN-SAT)
│ │ │ │ │ │ (0 to 6 are Sunday to Saturday; 7 is Sunday, the same as 0)
│ │ │ │ │ │ ┌──── (optional) year (1 - 9999)
│ │ │ │ │ │ │
* * * * * * *
- Optional second and year fields: 6 fields add seconds first, 7 fields also add a year last.
- L: Last day of month, or last occurrence of a weekday.
Lin day-of-month is the last day of the month,LWthe last weekday (Mon-Fri) of the month, and5#L,5LorFRI#Lthe last Friday of the month. - W: Nearest weekday.
15Wis the weekday closest to the 15th, never crossing into another month. - #: Nth occurrence of a weekday.
5#2is the second Friday,MON#1the first Monday of the month. - +: Prefix the day-of-week field to combine it with day-of-month using AND:
0 12 1 * +MONonly runs when the 1st is a Monday. - ?: An alias for
*. Non-portable, avoid it in patterns meant for other systems. - DST follows Vixie cron, deviating from OCPS 1.4 §4.3.1:
- Fixed-time jobs, with a single value in each of second, minute and hour (e.g.
0 30 2 * * *): a time that occurs twice (fall back) runs once, at the first occurrence. A time that doesn't exist (spring forward) runs at the first valid time after the gap, on the same day. - Interval jobs, with a wildcard, step, range or list in second, minute or hour
(e.g.
*/5 * * * *or0 30 1,2 * * *): a time that occurs twice runs on both occurrences. A time that doesn't exist is skipped. next_run(),next_runs(),previous_runs()andenumerate()move in real time. They never return a time they have passed, and cover the first pass through a repeated hour before the second.
- Fixed-time jobs, with a single value in each of second, minute and hour (e.g.
By default, day-of-month and day-of-week are combined with OR: 0 20 1 * MON runs on the 1st of the month
and on Mondays. Use the + modifier or dom_and_dow=True for AND.
| Field | Required | Allowed values | Special characters | Remarks |
|---|---|---|---|---|
| Seconds | Optional | 0-59 | * , - / ? | Defaults to 0 |
| Minutes | Yes | 0-59 | * , - / ? | |
| Hours | Yes | 0-23 | * , - / ? | |
| Day of Month | Yes | 1-31 | * , - / ? L W | L = last day, W = nearest weekday |
| Month | Yes | 1-12 or JAN-DEC | * , - / ? | |
| Day of Week | Yes | 0-7 or SUN-SAT | * , - / ? L # + | 0 and 7 = Sunday, 1-7 = Sunday-Saturday with alternative_weekdays |
| Year | Optional | 1-9999 | * , - / | Defaults to *. */2 matches even years, see below. |
Weekday and month names are case-insensitive.
Nicknames:
| Nickname | Same as |
|---|---|
| @yearly / @annually | 0 0 1 1 * |
| @monthly | 0 0 1 * * |
| @weekly | 0 0 * * 0 (Sundays, also with alternative_weekdays) |
| @daily / @midnight | 0 0 * * * |
| @hourly | 0 * * * * |
| @reboot | Recognized, but raises CronError, since a library can't detect system startup. |
OCPS conformance
Croner passes the OCPS conformance tests, with these deliberate deviations:
- DST: follows Vixie cron instead of OCPS 1.4 §4.3.1, as described above.
?is accepted in every field, as an alias for*. OCPS 1.4 only allows it in day-of-month and day-of-week.- Nicknames are case-insensitive,
@Dailyworks. OCPS 1.1 makes them case-sensitive. @rebootis rejected when the pattern is parsed. OCPS 1.1 accepts it at parse time and lets the runtime reject it.- Years: the supported range is 1-9999, wider than the portable 1970-2199. Wildcard steps in the
year field count from year 0 (which is never matched itself), so
*/2matches even years, as OCPS requires. Other steps can match other years than counting from 1970 would:*/4matches 2024, 2028, ..., where 1970-2199 gives 2026, 2030, .... Use a range, like2026-2199/4, to be portable.
Pattern objects
croner.CronPattern parses a pattern on its own. job.pattern is the parsed pattern of a job.
from croner import CronPattern
pattern = CronPattern("0 9-11 * * 1-5")
pattern.values("hour") # [9, 10, 11]
pattern.values("day_of_week") # [1, 2, 3, 4, 5]
pattern.is_fixed_time # False
Command line
python -m croner describes a pattern and lists its next runs, handy for checking a pattern before
putting it in a config file:
$ python -m croner "0 9 * * MON-FRI" --timezone Europe/Stockholm -n 3
At 09:00, on Monday, Tuesday, Wednesday, Thursday, and Friday.
2026-11-09T09:00:00+01:00
2026-11-10T09:00:00+01:00
2026-11-11T09:00:00+01:00
Use --start to list runs after another date, and --previous for previous runs.
Development
pip install -e . pytest mypy ruff
python -m pytest
TZ=America/New_York python -m pytest # jobs without a time zone use system local time
ruff check .
mypy
tools/compare_with_js.py is a differential test: it runs random patterns, options and dates around DST
changes through croner-py and the JavaScript implementation and compares the results. It needs Node.js
22.6 or newer, see the script for details.
License
MIT License
Metadata
Release files for croner 1.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 | Provenance | Actions |
|---|---|---|---|---|
| croner-1.1.0.tar.gz | 35.3 kB | With provenance | Details for croner-1.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| croner-1.1.0-py3-none-any.whl | Python 3 | none | any | Details for croner-1.1.0-py3-none-any.whl |
Total release size: 65.0 kB
Release files / croner-1.1.0.tar.gz
| Download URL | croner-1.1.0.tar.gz |
|---|---|
| Size | 35.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5b89eef230cd58525cc837c763e6fe6056b520d73c0d9253d574cf2e5fb5f19e
|
|
BLAKE2b-256 checksum How to use checksums |
47a1d6ba8c87725eaff57dffeeeb1117266160b093988eff8d110791ccd8f02b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Nov 7, 2026.
Transparency logRelease files / croner-1.1.0-py3-none-any.whl
| Download URL | croner-1.1.0-py3-none-any.whl |
|---|---|
| Size | 29.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
658f7c946fd586964df638323435edd6979e27745562f0433a83778222f68489
|
|
BLAKE2b-256 checksum How to use checksums |
50d79679d66fd98dc59bed3ba8445c6ea9e3063f0d57457df914ccb73cd16abe
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Nov 7, 2026.
Transparency logSecurity for croner 1.1.0
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.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for croner-1.1.0.tar.gz
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 Nov 7, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.