Skip to content

⏰ Modern datetime library for Python, written in Rust

License

Notifications You must be signed in to change notification settings

ariebovenberg/whenever

Repository files navigation

⏰ Whenever

Typed and DST-safe datetimes for Python, written in Rust

Do you cross your fingers every time you work with Python's datetime—hoping that you didn't mix naive and aware? or that you avoided its other pitfalls? or that you properly accounted for Daylight Saving Time (DST)? There’s no way to be sure...

✨ Until now! ✨

Whenever helps you write correct and type checked datetime code. Mistakes become red squiggles in your IDE, instead of bugs in production. It's also way faster than other third-party libraries—and usually the standard library as well.

Shows a bar chart with benchmark results.

RFC3339-parse, normalize, compare to now, shift, and change timezone (1M times)

⚠️ Note: Whenever is in pre-1.0 beta. The API may change as we gather feedback and improve the library. Leave a ⭐️ on github if you'd like to see how this project develops!

Why not the standard library?

Over 20+ years, Python's datetime has grown out of step with what you'd expect from a modern datetime library. Two points stand out:

  1. It doesn't always account for Daylight Saving Time (DST). Here is a simple example:

    bedtime = datetime(2023, 3, 25, 22, tzinfo=ZoneInfo("Europe/Paris"))
    full_rest = bedtime + timedelta(hours=8)
    # It returns 6am, but should be 7am—because we skipped an hour due to DST!

    Note this isn't a bug, but a design decision that DST is only considered when calculations involve two timezones. If you think this is surprising, you are not alone.

  2. Typing can't distinguish between naive and aware datetimes. Your code probably only works with one or the other, but there's no way to enforce this in the type system!

    # Does this expect naive or aware? Can't tell!
    def schedule_meeting(at: datetime) -> None: ...

Why not other libraries?

There are two other popular third-party libraries, but they don't (fully) address these issues. Here's how they compare to whenever and the standard library:

Whenever datetime Arrow Pendulum
DST-safe ⚠️
Typed aware/naive
Fast

Arrow is probably the most historically popular 3rd party datetime library. It attempts to provide a more "friendly" API than the standard library, but doesn't address the core issues: it keeps the same footguns, and its decision to reduce the number of types to just one (arrow.Arrow) means that it's even harder for typecheckers to catch mistakes.

Pendulum arrived on the scene in 2016, promising better DST-handling, as well as improved performance. However, it only fixes some DST-related pitfalls, and its performance has significantly degraded over time. Additionally, it hasn't been actively maintained since a breaking 3.0 release last year.

Why use whenever?

  • 🌐 DST-safe arithmetic
  • 🛡️ Typesafe API prevents common bugs
  • ✅ Fixes issues arrow/pendulum don't
  • ⚖️ Based on proven and familiar concepts
  • ⚡️ Unmatched performance
  • 💎 Thoroughly tested and documented
  • 📆 Support for date arithmetic
  • ⏱️ Nanosecond precision
  • 🦀 Rust!—but with a pure-Python fallback
  • 🚀 Support for the latest GIL-related improvements (experimental)

Quickstart

>>> from whenever import (
...    # Explicit types for different use cases
...    Instant,
...    ZonedDateTime,
...    LocalDateTime,
... )

# Identify moments in time, without timezone/calendar complexity
>>> now = Instant.now()
Instant(2024-07-04 10:36:56Z)

# Simple, explicit conversions
>>> now.to_tz("Europe/Paris")
ZonedDateTime(2024-07-04 12:36:56+02:00[Europe/Paris])

# A 'naive' local time can't accidentally mix with other types.
# You need to explicitly convert it and handle ambiguity.
>>> party_invite = LocalDateTime(2023, 10, 28, hour=22)
>>> party_invite.add(hours=6)
Traceback (most recent call last):
  ImplicitlyIgnoringDST: Adjusting a local datetime implicitly ignores DST [...]
>>> party_starts = party_invite.assume_tz("Europe/Amsterdam", disambiguate="earlier")
ZonedDateTime(2023-10-28 22:00:00+02:00[Europe/Amsterdam])

# DST-safe arithmetic
>>> party_starts.add(hours=6)
ZonedDateTime(2022-10-29 03:00:00+01:00[Europe/Amsterdam])

# Comparison and equality
>>> now > party_starts
True

# Formatting & parsing common formats (ISO8601, RFC3339, RFC2822)
>>> now.format_rfc2822()
"Thu, 04 Jul 2024 10:36:56 GMT"

# If you must: you can convert to/from the standard lib
>>> now.py_datetime()
datetime.datetime(2024, 7, 4, 10, 36, 56, tzinfo=datetime.timezone.utc)

Read more in the feature overview or API reference.

Roadmap

  • 🧪 0.x: get to feature-parity, process feedback, and tweak the API:

    • ✅ Datetime classes
    • ✅ Deltas
    • ✅ Date and time of day (separate from datetime)
    • ✅ Implement Rust extension for performance
    • 🚧 Parsing leap seconds
    • 🚧 Improved parsing and formatting
    • 🚧 More helpful error messages
    • 🚧 Intervals
  • 🔒 1.0: API stability and backwards compatibility

Limitations

  • Supports the proleptic Gregorian calendar between 1 and 9999 AD
  • Timezone offsets are limited to whole seconds (consistent with IANA TZ DB)
  • No support for leap seconds (consistent with industry standards and other modern libraries)

Versioning and compatibility policy

Whenever follows semantic versioning. Until the 1.0 version, the API may change with minor releases. Breaking changes will be meticulously explained in the changelog. Since the API is fully typed, your typechecker and/or IDE will help you adjust to any API changes.

⚠️ Note: until 1.x, pickled objects may not be unpicklable across versions. After 1.0, backwards compatibility of pickles will be maintained as much as possible.

License

Whenever is licensed under the MIT License. The binary wheels contain Rust dependencies which are licensed under MIT or Apache 2.0. These licenses are included in the wheels and source distributions.

Acknowledgements

This project is inspired by the following projects. Check them out!

The benchmark comparison graph is based on the one from the Ruff project.

Contributing

Contributions are welcome! Please open an issue or a pull request.

⚠️ Note: Non-trivial changes should be discussed in an issue first. This is to avoid wasted effort if the change isn't a good fit for the project.

⚠️ Note: Some tests are skipped on Windows. These tests use unix-specific features to set the timezone for the current process. As a result, Windows isn't able to run certain tests that rely on the system timezone. It appears that this functionality (only needed for the tests) is not available on Windows.

Setting up a development environment

An example of setting up things up:

# install the dependencies
make init

# build the rust extension
make build

make test  # run the tests (Python and Rust)
make format  # apply autoformatting
make ci-lint  # various static checks
make typecheck  # run mypy and typing tests