Open-source project
pallets-eco/croniter avatar
pallets-eco/croniter

croniter: iterating over cron schedules as datetime objects

Parses cron schedules to iterate over datetime objects.

565 stars136 forksPythonMIT

At a glance

What is it?
croniter turns a cron expression into an iterator that yields the next or previous matching datetime, with support for the OR and AND day semantics, strict validation, last day of month and nearest weekday. It is a small, well documented library with one runtime dependency, and it is part of the Pallets ecosystem.
Who is it for?
Adopt croniter if you need to compute when a cron expression fires, in Python, and you want the same day-of-month and day-of-week semantics the system cron uses rather than a looser approximation. Do not adopt it if you need to parse crontab files with their environment assignments, because the library takes an expression rather than a file, and it does not schedule anything itself.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 7 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One constructor and one method do the work

croniter provides iteration for the datetime object with a cron like format, and the README makes the point that all you need to know is how to use the constructor and the get_next method.

The signatures are given directly in the documentation:

python
    >>> def __init__(self, cron_format, start_time=time.time(), day_or=True)
python
    >>> def get_next(self, ret_type=float)

croniter iterates along with the cron_format from the start_time, and the format is min hour day month day_of_week, with the Wikipedia cron page linked for the details. get_next calculates the next value according to the cron expression and returns an object of the given type, and that type should be a float or a datetime object.

The float option is the one that catches people out, because it is the default. A cron schedule is really a set of instants, and asking for the next one as a float timestamp is occasionally what you want and more often a source of an off-by-one timezone bug. The examples in the README always pass datetime explicitly, which is the safer habit.

A basic session looks like this, with the base time and a five minute schedule:

python
    >>> from croniter import croniter
    >>> from datetime import datetime
    >>> base = datetime(2010, 1, 25, 4, 46)
    >>> iter = croniter('*/5 * * * *', base)  # every 5 minutes
    >>> print(iter.get_next(datetime))   # 2010-01-25 04:50:00
    >>> print(iter.get_next(datetime))   # 2010-01-25 04:55:00
    >>> print(iter.get_next(datetime))   # 2010-01-25 05:00:00

Note that the same iterator instance is used for each call, so it advances. That is the whole model: create it against a base time, then ask for the next value as many times as you need.

The day_or switch is the one semantic decision you have to make

The day_or switch controls how croniter handles the day and day_of_week entries, and the README is explicit about the default. The default option is the cron behaviour, which connects those values using OR. If the switch is set to False, the values are connected using AND, and this behaves like fcron and enables you to define a job that executes each 2nd Friday of a month by setting the days of month and the weekday.

The difference is visible in two examples that look almost identical. With OR, the schedule 2 4 1 * wed means 04:02 on every Wednesday OR on the 1st day of the month:

python
    >>> iter = croniter('2 4 1 * wed', base)  # 04:02 on every Wednesday OR on 1st day of month
    >>> print(iter.get_next(datetime))   # 2010-01-27 04:02:00
    >>> print(iter.get_next(datetime))   # 2010-02-01 04:02:00
    >>> print(iter.get_next(datetime))   # 2010-02-03 04:02:00

With day_or=False, the same expression means the 1st day of the month if it is a Wednesday, and the results are months apart rather than days:

python
    >>> iter = croniter('2 4 1 * wed', base, day_or=False)  # 04:02 on every 1st day of the month if it is a Wednesday
    >>> print(iter.get_next(datetime))   # 2010-09-01 04:02:00
    >>> print(iter.get_next(datetime))   # 2010-12-01 04:02:00
    >>> print(iter.get_next(datetime))   # 2011-06-01 04:02:00

Getting this wrong is the classic croniter bug, and it is easy to get wrong because the default is the OR behaviour, which is what the man page for crontab says and what most people assume. If you want a job to run on a particular weekday of a particular month, the default will fire on the wrong days.

Going backwards, and validating before you schedule

get_prev is supported and behaves as the mirror of get_next, and the README gives a worked example of it:

python
    >>> base = datetime(2010, 8, 25)
    >>> itr = croniter('0 0 1 * *', base)
    >>> print(itr.get_prev(datetime))  # 2010-08-01 00:00:00
    >>> print(itr.get_prev(datetime))  # 2010-07-01 00:00:00
    >>> print(itr.get_prev(datetime))  # 2010-06-01 00:00:00

That is useful for two things that come up often: finding the previous run time of a job to report on it, and computing a lookback window for a query.

For validation there is an is_valid class method, and the simple version checks only that each field parses:

python
    >>> croniter.is_valid('0 0 1 * *')  # True
    >>> croniter.is_valid('0 wrong_value 1 * *')  # False

The strict mode is the more interesting part, and it is a recent addition. By default, is_valid and expand only check that each field is within its own range, for example day 1 to 31 and month 1 to 12. They do not cross-validate fields, so an expression like 0 0 31 2 * for February 31st is considered valid even though it can never match a real date. Use strict=True to enable cross-field validation that rejects impossible day and month combinations:

python
    >>> croniter.is_valid('0 0 31 2 *')  # True (default: syntax only)
    >>> croniter.is_valid('0 0 31 2 *', strict=True)  # False (Feb has at most 29 days)
    >>> croniter.is_valid('0 0 31 4 *', strict=True)  # False (Apr has 30 days)
    >>> croniter.is_valid('0 0 30 1,2 *', strict=True)  # True (day 30 is valid in Jan)

For user supplied expressions, strict mode is the difference between a job that never runs and a job that runs as expected.

Leap years are handled explicitly, which is more than most crons do

The strict validation section pays attention to February, and the rules are spelled out precisely.

When strict=True and the expression does not include a year field, February is assumed to have 29 days, since leap years exist:

python
    >>> croniter.is_valid('0 0 29 2 *', strict=True)  # True (leap years exist)

If the expression includes a year field in the 7-field format, the leap year status is determined from the specified years:

python
    >>> croniter.is_valid('0 0 29 2 * 0 2024', strict=True)  # True (2024 is a leap year)
    >>> croniter.is_valid('0 0 29 2 * 0 2023', strict=True)  # False (2023 is not)
    >>> croniter.is_valid('0 0 29 2 * 0 2023-2025', strict=True)  # True (2024 in range is a leap year)

And for 5 or 6 field expressions you can pass strict_year to provide the year or years for the leap year check without adding a year field to the expression itself:

python
    >>> croniter.is_valid('0 0 29 2 *', strict=True, strict_year=2024)  # True (leap year)
    >>> croniter.is_valid('0 0 29 2 *', strict=True, strict_year=2023)  # False (not a leap year)
    >>> croniter.is_valid('0 0 29 2 *', strict=True, strict_year=[2023, 2024])  # True (2024 is a leap year)

The strict and strict_year parameters are also available on expand(), where an invalid expression raises CroniterBadCronError:

python
    >>> croniter.expand('0 0 31 2 *', strict=True)  # raises CroniterBadCronError

The year range syntax with a dash is a small detail that makes the strict mode genuinely useful for a scheduled job that runs for a known window, rather than for ever.

Two day-of-month extensions beyond plain cron

The last two features in the README are extensions to the day-of-month field that go beyond what a standard crontab gives you.

The first is l for the last day of the month, and it automatically respects the number of days in each month, including February and leap years:

python
    >>> croniter('0 0 l * *', datetime(2024, 1, 30)).get_next(datetime)
    datetime.datetime(2024, 1, 31, 0, 0)
    >>> croniter('0 0 l * *', datetime(2024, 2, 1)).get_next(datetime)  # leap year
    datetime.datetime(2024, 2, 29, 0, 0)
    >>> croniter('0 0 l * *', datetime(2023, 2, 1)).get_next(datetime)  # non-leap year
    datetime.datetime(2023, 2, 28, 0, 0)

A monthly reconciliation job, or a job that runs on the last working moment of a month, is one character away with this and awkward without it.

The second is the W character, which specifies the nearest weekday, Monday to Friday, to the given day, and both the nW and the Wn formats are accepted. The example in the README uses a base of June 1, 2024.

So between them, l and W cover the two day-of-month cases that come up in practice and that plain cron leaves you to compute yourself. Neither is standard crontab syntax, so a schedule using them will not transfer to a system cron, and that portability cost is worth keeping in mind before you use them in a configuration that has to run in two places.

A small package with one dependency and a real release process

croniter is part of the Pallets Community Ecosystem, and the README explains the arrangement. Pallets is the open source organisation that maintains Flask, Pallets-Eco enables community maintenance of Flask extensions, and the project is inviting people to help maintain it on the Pallets Discord server.

That context explains a lot about the shape of the project. The runtime dependencies section in the manifest lists exactly one entry, python_dateutil, and the licence is MIT with the licence file listed. It requires Python 3.9 or newer, and the classifiers cover 3.9 through 3.13, with the development status marked as Beta.

The tooling is conventional Pallets practice. The build is hatchling with pinned build requirements, the tests use pytest with pytest-cov and coverage, and there are separate dependency groups for dev, lint, mypy, format, tox and release. Ruff does both linting and formatting with a line length of 99. The version in the manifest is 6.3.0.dev0, and the release is handled by zest.releaser, with a CHANGELOG.rst and a release.sh in the repository root.

The Docker setup is there for the test matrix rather than for end users. There is a Dockerfile based on debian:bookworm-slim that installs uv and tox, and a compose file that mounts the source, the tox configuration and the entry script, with variants for 32-bit builds. The runtime dependencies are simple enough that most users will just install the package.

The recent release history shows the pace: 6.2.4 on 2026-07-10, and 6.2.3 and 6.2.2 both on 2026-07-02, with the last push on 2026-10-16.

Editorial conclusion

Adopt croniter if you need to compute when a cron expression fires, in Python, and you want the same day-of-month and day-of-week semantics the system cron uses rather than a looser approximation. Do not adopt it if you need to parse crontab files with their environment assignments, because the library takes an expression rather than a file, and it does not schedule anything itself. Before you rely on it, decide whether your day fields should be OR or AND, since the default is OR and the AND behaviour is opt-in, and enable strict validation if your expressions are user supplied.

Frequently asked questions

How do I get the next matching datetime from a cron expression with croniter?

Create a croniter with the expression and a start time, then call get_next(datetime) repeatedly, since the iterator advances with each call. The constructor signature is __init__(self, cron_format, start_time=time.time(), day_or=True) and get_next takes a ret_type that should be a float or a datetime.

What does the day_or parameter do in croniter?

It controls how the day and day_of_week fields are combined. The default of True uses the cron behaviour, connecting them with OR, so 2 4 1 * wed fires on every Wednesday or on the 1st of the month. Setting it to False connects them with AND, so the same expression fires on the 1st only when it is a Wednesday.

How do I validate a cron expression in croniter?

Use the is_valid class method. By default it only checks each field's own range, so 0 0 31 2 * is considered valid. Pass strict=True for cross-field validation that rejects impossible dates, and strict_year to supply the year or years for the leap year check without adding a year field.

Does croniter support the last day of the month and nearest weekday?

Yes. You can use l in the day-of-month field to mean the last day of the month, which respects the actual number of days including leap years, and the W character to specify the nearest weekday to a given day, in both the nW and Wn formats.

Official sources

  1. Issues
  2. License: MIT
  3. pallets-eco/croniter on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/pallets-eco-croniter.svg)](https://hysenlabs.com/projects/pallets-eco-croniter)

Was this useful?