Skip to main content

Foolproof datetimes for maintainable code

Project description

https://img.shields.io/pypi/v/whenever.svg?style=flat-square&color=blue https://img.shields.io/pypi/pyversions/whenever.svg?style=flat-square https://img.shields.io/pypi/l/whenever.svg?style=flat-square&color=blue https://img.shields.io/badge/mypy-strict-forestgreen?style=flat-square https://img.shields.io/badge/coverage-100%25-forestgreen?style=flat-square https://img.shields.io/github/actions/workflow/status/ariebovenberg/whenever/tests.yml?branch=main&style=flat-square https://img.shields.io/readthedocs/whenever.svg?style=flat-square

Foolproof datetimes for maintainable Python code

Do you cross your fingers every time you work with datetimes, hoping that you didn’t mix naive and aware? or that you converted to UTC everywhere? or that you avoided the many pitfalls of the standard library? There’s no way to be sure, until you run your code…

✨ Until now! ✨

Whenever is a datetime library designed from the ground up to enforce correctness. Mistakes become red squiggles in your IDE, instead of production outages.

Benefits

  • Distinct classes with well-defined behavior

  • Fixes timezone quirks that even pendulum doesn’t address

  • Enforce correctness without runtime checks

  • Based on familiar concepts from other languages. Doesn’t reinvent the wheel

  • Simple and obvious. No frills or surprises

  • Thoroughly documented and tested

  • No dependencies

Quickstart

>>> from whenever import (
...    # Explicit types for different use cases
...    UTCDateTime,     # -> Great for codebases that normalize to UTC
...    OffsetDateTime,  # -> Localized times without ambiguities
...    ZonedDateTime,   # -> Full-featured IANA timezone support
...    LocalDateTime,   # -> In the local system timezone
...    NaiveDateTime,   # -> Detached from any timezone
...
...    hours, days, minutes  # aliases for timedelta(hours=...) etc.
... )

>>> py311_release = UTCDateTime(2022, 10, 24, hour=17)
UTCDateTime(2022-10-24 17:00:00Z)
>>> pycon23_started = OffsetDateTime(2023, 4, 21, hour=9, offset=hours(-6))
OffsetDateTime(2023-04-21 09:00:00-06:00)

# Simple, explicit conversions
>>> py311_in_paris = py311_release.as_zoned("Europe/Paris")
ZonedDateTime(2022-10-24 19:00:00+02:00[Europe/Paris])
>>> pycon23_started.as_local()
LocalDateTime(2023-04-21 11:00:00-04:00)  # system timezone in NYC here

# Comparison and equality across aware types
>>> pycon23_started < py311_release
False
>>> py311_release == py311_release.as_zoned("America/Los_Angeles")
True

# DST-aware addition/subtraction
>>> py311_in_paris + days(7)
ZonedDateTime(2022-10-31 18:00:00+01:00[Europe/Paris])

# Naive type that can't accidentally be mixed with aware types
>>> simulation_start = NaiveDateTime(1950, 1, 1, hour=9)
>>> # Mistakes caught by typechecker:
>>> py311_release - simulation_start
>>> simulation_start == pycon23_started

# round-trip to and from strings
>>> py311_release.canonical_str()
'2022-10-24T17:00:00Z'
>>> ZonedDateTime.from_canonical_str('2022-10-24T19:00:00+02:00[Europe/Paris]')
ZonedDateTime(2022-10-24 19:00:00+02:00[Europe/Paris])

# If you must: you can access the underlying datetime object
>>> pycon23_started.py.ctime()
'Fri Apr 21 09:00:00 2023'

Read more in the full overview or API reference.

Why not…?

The standard library

The standard library is full of quirks and pitfalls. To summarize the detailed blog post:

  1. Incompatible concepts are squeezed into one class

  2. Operations ignore Daylight Saving Time (DST)

  3. The meaning of “naive” is inconsistent

  4. Non-existent datetimes pass silently

  5. It guesses in the face of ambiguity

  6. Disambiguation breaks equality

  7. Inconsistent equality within timezone

  8. datetime inherits from date

  9. datetime.timezone isn’t a timezone. ZoneInfo is.

  10. The local timezone is DST-unaware

Pendulum

Pendulum is full-featured datetime library, but it’s hamstrung by the decision to inherit from the standard library datetime. This means it inherits most of its pitfalls, with the notable exception of DST-aware addition/subtraction.

Arrow

Arrow is probably the most historically popular datetime library. Pendulum did a good write-up of the issues with Arrow. It addresses fewer of datetime’s pitfalls than Pendulum.

Maya

It’s unmaintained, but does have an interesting approach. By enforcing UTC, it bypasses a lot of issues with the standard library. To do so, it sacrifices the ability to represent offset, zoned, and local datetimes. So in order to perform any timezone-aware operations, you need to convert to the standard library datetime first, which reintroduces the issues.

DateType

DateType mostly fixes the issue of mixing naive and aware datetimes, and datetime/date inheritance during type-checking, but doesn’t address the other pitfalls. Additionally, it isn’t able to fully type-check all cases.

python-dateutil

Dateutil attempts to solve some of the issues with the standard library. However, it only adds functionality to work around the issues, instead of removing the pitfalls themselves. This still puts the burden on the developer to know about the issues, and to use the correct functions to avoid them. Without removing the pitfalls, it’s still very likely to make mistakes.

Heliclockter

This library is a lot more explicit about the different types of datetimes, addressing issue of naive/aware mixing with UTC, local, and zoned datetime subclasses. It doesn’t address the other datetime pitfalls though. Additionally, its “local” type doesn’t account for DST.

FAQs

Why isn’t it a drop-in replacement for the standard library?

Fixing the issues with the standard library requires a different API. Keeping the same API would mean that the same issues would remain.

Why not inherit from datetime?

Not only would this keep most of the issues with the standard library, it would result in brittle code: many popular libraries expect datetime exactly, and don’t work with subclasses.

What is the performance impact?

Because whenever wraps the standard library, head-to-head performance will always be slightly slower. However, because whenever removes the need for many runtime checks, it may result in a net performance gain in real-world applications.

Why not a C or Rust extension?

It actually did start out as a Rust extension. But since the wrapping code is so simple, it didn’t make much performance difference. Since it did make the code a lot more complex, a simple pure-Python implementation was preferred. If more involved operations are needed in the future, we can reconsider.

Is this production-ready?

The core functionality is complete and stable and the goal is to reach 1.0 soon. The API may change slightly until then. Of course, it’s still a relatively young project, so the stability relies on you to try it out and report any issues!

Versioning and compatibility policy

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

Acknowledgements

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

Contributing

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

An example of setting up things and running the tests:

poetry install
pytest

⚠️ Note: The tests don’t run on Windows yet. This is because the tests use unix-specific features to set the timezone for the current process. It can be made to work on Windows too, but I haven’t gotten around to it yet.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

whenever-0.2.1.tar.gz (20.5 kB view details)

Uploaded Source

Built Distribution

whenever-0.2.1-py3-none-any.whl (21.5 kB view details)

Uploaded Python 3

File details

Details for the file whenever-0.2.1.tar.gz.

File metadata

  • Download URL: whenever-0.2.1.tar.gz
  • Upload date:
  • Size: 20.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.11.5

File hashes

Hashes for whenever-0.2.1.tar.gz
Algorithm Hash digest
SHA256 eb5941e6bcee8533365c6e0e767f53c963254a5c93a4f666e96f3183001461d2
MD5 2a807e92bf1d27d15bf999a83b4af7b6
BLAKE2b-256 70b8f1481b6d5b4b74af0c49450bb2466a831c2a8892ed85611254fa535c2b0e

See more details on using hashes here.

File details

Details for the file whenever-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: whenever-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 21.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.11.5

File hashes

Hashes for whenever-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 5949b524695befab4610caa81a661eb0246e85833a8ae2caa90478b3354b2e93
MD5 7e87a2bf4acbc41a5d79bd6c2daf0639
BLAKE2b-256 54b99b00e1c76069185a543af2c10cb13dcf1f473f8ef300b8b2d782af271061

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page