Skip to main content

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 def functions both work.
  • Time zone aware. Returns aware datetime objects in any zoneinfo time 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 Z or an offset, are exact instants.
  • Naive datetimes, dates, and strings without an offset are wall clock times in the job's time zone (the timezone or utc_offset option), 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 tzinfo as 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. L in day-of-month is the last day of the month, LW the last weekday (Mon-Fri) of the month, and 5#L, 5L or FRI#L the last Friday of the month.
  • W: Nearest weekday. 15W is the weekday closest to the 15th, never crossing into another month.
  • #: Nth occurrence of a weekday. 5#2 is the second Friday, MON#1 the first Monday of the month.
  • +: Prefix the day-of-week field to combine it with day-of-month using AND: 0 12 1 * +MON only 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 * * * * or 0 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() and enumerate() move in real time. They never return a time they have passed, and cover the first pass through a repeated hour before the second.

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, @Daily works. OCPS 1.1 makes them case-sensitive.
  • @reboot is 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 */2 matches even years, as OCPS requires. Other steps can match other years than counting from 1970 would: */4 matches 2024, 2028, ..., where 1970-2199 gives 2026, 2030, .... Use a range, like 2026-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)

Source distribution for croner 1.1.0
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)

Table of built distributions (wheels) for croner 1.1.0
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

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