⏰ Whenever Type-safe datetimes for Python that get DST right. Rust or pure Python, your choice. Do you cross your fingers every time you work with Python's datetime—hoping that you didn't mix naive and aware, or run into one of its other pitfalls? Whenever takes the guesswork out, bringing well-established concepts from modern libraries in other languages to Python. Mixing up naive and aware becomes a typechecker…
Type-safe datetimes for Python that get DST right. Rust or pure Python, your choice.
Do you cross your fingers every time you work with Python’s datetime—hoping that you didn’t
mix naive and aware, or run into one of its other pitfalls?
Whenever takes the guesswork out, bringing well-established concepts from
modern libraries in other languages to Python.
Mixing up naive and aware becomes a typechecker error instead of a bug you find in production,
and DST is handled correctly in all arithmetic.
It’s also way faster than other third-party libraries, and usually the standard library as well.
Rather not depend on a Rust extension? A pure-Python backend is available too.
Parse, normalize, compare to now, shift, change time zone, and format (1M times)
⚠️ Note: 0.11 ships the 1.0 API. The deprecated spellings keep working
as compatibility shims until 1.0, and the changelog’s migration table lists each one.
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:
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 time zones.
If you think this is surprising, you
arenotalone.
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
puts a friendlier API on top of the standard library’s model — pitfalls
included. Its single do-it-all type and anything-goes arrow.get()
make mistakes harder for type checkers to catch, not easier.
Full comparison →
Pendulum
promised to fix datetime as a drop-in replacement — but a subclass
can’t change behavior and stay drop-in. The result is an API built on
guesswork: missing time zones become UTC, incomplete input is completed
from the clock, and until 3.3, + inspected the call stack to guess which
semantics the caller expected. Bugs follow: today() can even return yesterday.
Full comparison →
Whenever supports Python 3.10 and newer. Platform wheels include the Rust
extension; a pure-Python wheel covers other platforms, and PyPy and GraalVM
load the pure-Python backend automatically. To choose the pure-Python backend
yourself, see the
FAQ.
Quickstart
>>> from whenever import (
... # Explicit types for different use cases
... Instant,
... ZonedDateTime,
... PlainDateTime,
... )
# Identify moments in time, without time zone/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 plain (local) datetime can't accidentally mix with exact types.
# You need to explicitly convert it and handle ambiguity.
>>> party_invite = PlainDateTime("2023-10-28 22:00")
>>> party_invite.add(hours=6)
NaiveArithmeticWarning: Shifting a PlainDateTime by exact time units does not account for time zone transitions [...]
PlainDateTime("2023-10-29 04:00:00")
>>> party_starts = party_invite.assume_tz("Europe/Amsterdam")
ZonedDateTime("2023-10-28 22:00:00+02:00[Europe/Amsterdam]")
# DST-safe arithmetic
>>> party_starts.add(hours=6)
ZonedDateTime("2023-10-29 03:00:00+01:00[Europe/Amsterdam]")
# Comparison and equality
>>> now > party_starts
True
# Rounding and truncation
>>> now.round("minute", increment=15)
Instant("2024-07-04 10:30:00Z")
# Formatting & parsing common formats (ISO 8601, RFC 3339, RFC 2822)
>>> now.format_rfc2822()
"Thu, 04 Jul 2024 10:36:56 GMT"
# Custom pattern formatting and parsing
>>> party_starts.format("MMM DD, HH:mm zz")
"Oct 28, 22:00 CEST"
# If you must: you can convert to/from the standard lib
>>> now.to_stdlib()
datetime.datetime(2024, 7, 4, 10, 36, 56, tzinfo=datetime.timezone.utc)
Supports the proleptic Gregorian calendar between 1 and 9999 AD
Time zone offsets are limited to whole seconds (consistent with the IANA time zone database)
Stability policy
Whenever follows semantic versioning.
Version 0.11 establishes the 1.0 API: unless significant issues arise,
1.0 differs from it only by removing the spellings 0.11 deprecates.
Breaking changes are meticulously explained in the changelog.
Since the API is fully typed, your typechecker and/or IDE
will help you adjust to any API changes.
License
Whenever is licensed under the MIT License.
The binary wheels contain Rust dependencies which are licensed under
similarly permissive licenses (MIT, Apache-2.0, and others).
For more details, see the licenses included in the distribution.
Acknowledgements
Whenever draws from decades of datetime library design across multiple languages:
Noda Time and Joda Time
pioneered the concept-per-type approach that makes whenever possible.
Noda Time’s type hierarchy directly inspired whenever’s design.
Temporal (JavaScript proposal)
provided inspiration for handling complex cases around DST ambiguity and rounding.
After years of TC39 design work, Temporal’s API
is extraordinarily thorough. Whenever benefits from those hard-won insights.
Python’s datetime module is used extensively in whenever’s pure-Python backend
for low-level date/time handling.
Whenever also borrows a few nifty ideas from Jiff:
A modern datetime library in Rust which takes inspiration from Temporal.
The benchmark comparison graph is adapted from the Ruff project.
评论