# ⏰ Whenever¶
Typed and DST-safe datetimes for Python, available in Rust or pure Python.
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? There’s no way to be sure…
✨ Until now! ✨
Whenever helps you write correct and type checked datetime code, using well-established concepts from modern libraries in other languages. It’s also way faster than other third-party libraries, and usually the standard library as well. Don’t buy the Rust hype?—don’t worry: a pure Python version is available as well.
Parse, normalize, compare to now, shift, change timezone, and format (1M times)
📖 Docs | 🐍 PyPI | 🐙 GitHub | 🚀 Changelog | ❓ FAQ | 🗺️ Roadmap | 💬 Issues & feedback
> ⚠️ Note: A 1.0 release is expected this year. Until then, 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’s in a long maintenance slump with only two releases in the last four years, while many serious and long-standing issues remain unaddressed.
## 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
  * 🪃 Pydantic support (preview)
  * 🦀 Rust!—but with a pure-Python option
  * 🚀 Supports per-interpreter GIL


## Quickstart¶
    
    >>> from whenever import (
    ...    # Explicit types for different use cases
    ...    Instant,
    ...    ZonedDateTime,
    ...    PlainDateTime,
    ... )
    
    # 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' datetime can't accidentally mix with other types.
    # You need to explicitly convert it and handle ambiguity.
    >>> party_invite = PlainDateTime("2023-10-28 22:00")
    >>> 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")
    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 (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
    * 🚧 Tweaks to the delta API
  * 🔒 1.0: API stability and backwards compatibility
    * 🚧 Customizable parsing and formatting
    * 🚧 Intervals
    * 🚧 Ranges and recurring times
    * 🚧 Parsing leap seconds


## 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.
## 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 stands on the shoulders of giants:
  * Its core datamodel takes big inspiration from Noda Time, which in turn is inspired by the influential Joda Time.
  * Whenever also takes a lot of inspiration from the Temporal proposal for JavaScript. After years of design work and gathering feedback, TC39 has come up with an extraodrinarily well-specified API fit for a dynamic language. Whenever takes a lot of cues from Temporal for complex APIs such as handling ambiguity and rounding.
  * The pure-Python version of whenever also makes extensive use of Python’s `datetime` and `zoneinfo` libraries internally.
  * 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.


# Contents¶
## 🎯 Examples¶
This page contains small, practical examples of using `whenever`. For more in-depth information, refer to the overview.
### Get the current time in UTC¶
    
    >>> from whenever import Instant
    >>> Instant.now()
    Instant("2025-04-19 19:02:56.39569Z")
    
### Convert UTC to the system timezone¶
    
    >>> from whenever import Instant
    >>> i = Instant.now()
    >>> i.to_system_tz()
    ZonedDateTime("2025-04-19 21:02:56.39569+02:00[Europe/Berlin]")
    
### Convert from one timezone to another¶
    
    >>> from whenever import ZonedDateTime
    >>> d = ZonedDateTime(2025, 4, 19, hour=15, tz="America/New_York")
    >>> d.to_tz("Europe/Berlin")
    ZonedDateTime("2025-04-19 21:00:00+02:00[Europe/Berlin]")
    
### Convert a date to datetime¶
    
    >>> from whenever import Date, Time
    >>> date = Date(2023, 10, 1)
    >>> date.at(Time(12, 30))
    PlainDateTime("2023-10-01 12:30:00")
    
### Calculate somebody’s age¶
    
    >>> from whenever import Date
    >>> birth_date = Date(2023, 11, 2)
    >>> age = Date.today_in_system_tz() - birth_date
    DateDelta("P1y5m26d")
    >>> months, days = age.in_months_days()
    (17, 26)
    >>> age.in_years_months_days()
    (1, 5, 26)
    
### Assign a timezone to a datetime¶
    
    >>> from whenever import PlainDateTime
    >>> datetime = PlainDateTime(2023, 10, 1, 12, 30)
    >>> datetime.assume_tz("America/New_York")
    ZonedDateTime("2023-10-01 12:30:00-04:00[America/New_York]")
    
### Integrate with the standard library¶
    
    >>> import datetime
    >>> py_dt = datetime.datetime.now(datetime.UTC)
    >>> from whenever import Instant
    >>> # create an Instant from any aware datetime
    >>> i = Instant.from_py_datetime(py_dt)
    Instant("2025-04-19 19:02:56.39569Z")
    >>> zdt = i.to_tz("America/New_York")
    ZonedDateTime("2025-04-19 15:02:56.39569-04:00[America/New_York]")
    >>> # convert back to the standard library
    >>> zdt.py_datetime()
    datetime.datetime(2025, 4, 19, 15, 2, 56, 395690, tzinfo=ZoneInfo('America/New_York'))
    
### Parse an ISO8601 datetime string¶
    
    >>> from whenever import Instant
    >>> Instant.parse_iso("2025-04-19T19:02+04:00")
    Instant("2025-04-19 15:02:00Z")
    
Or, if you want to keep the offset value:
    
    >>> from whenever import OffsetDateTime
    >>> OffsetDateTime.parse_iso("2025-04-19T19:02+04:00")
    OffsetDateTime("2025-04-19 19:02:00+04:00")
    
### Determine the start of the hour¶
    
    >>> d = ZonedDateTime.now("America/New_York")
    ZonedDateTime("2025-04-19 15:46:41-04:00[America/New_York]")
    >>> d.round("hour", mode="floor")
    ZonedDateTime("2025-04-19 15:00:00-04:00[America/New_York]")
    
The `round()` method can be used for so much more! See its documentation for more details.
### Get the current unix timestamp¶
    
    >>> from whenever import Instant
    >>> i = Instant.now()
    >>> i.timestamp()
    1745090505
    
Note that this is always in whole seconds. If you need additional precision:
    
    >>> i.timestamp_millis()
    1745090505629
    >>> i.timestamp_nanos()
    1745090505629346833
    
### Get a date and time from a timestamp¶
    
    >>> from whenever import ZonedDateTime
    >>> ZonedDateTime.from_timestamp(1745090505, tz="America/New_York")
    ZonedDateTime("2025-04-19 15:21:45-04:00[America/New_York]")
    
### Find the duration between two datetimes¶
    
    >>> from whenever import ZonedDateTime
    >>> d = ZonedDateTime(2025, 1, 3, hour=15, tz="America/New_York")
    >>> d2 = ZonedDateTime(2025, 1, 5, hour=8, minute=24, tz="Europe/Paris")
    >>> d2 - d
    TimeDelta("PT35h24m")
    
### Move a date by six months¶
    
    >>> from whenever import Date
    >>> date = Date(2023, 10, 31)
    >>> date.add(months=6)
    Date("2024-04-30")
    
### Discard fractional seconds¶
    
    >>> from whenever import Instant
    >>> i = Instant.now()
    Instant("2025-04-19 19:02:56.39569Z")
    >>> i.round()
    Instant("2025-04-19 19:02:56Z")
    
Use the arguments of `round()` to customize the rounding behavior.
### Handling ambiguous datetimes¶
Due to daylight saving time, some date and time values don’t exist, or occur twice in a given timezone. In the example below, the clock was set forward by one hour at 2:00 AM, so the time 2:30 AM doesn’t exist.
    
    >>> from whenever import ZonedDateTime
    >>> # set up the date and time for the example
    >>> dt = PlainDateTime(2023, 2, 26, hour=2, minute=30)
    
The default behavior (take the first offset) is consistent with other modern libraries and industry standards:
    
    >>> zoned = dt.assume_tz("Europe/Berlin")
    ZonedDateTime("2023-02-26 03:30:00+02:00[Europe/Berlin]")
    
But it’s also possible to “refuse to guess” and choose the “earlier” or “later” occurrence explicitly:
    
    >>> zoned = dt.assume_tz("Europe/Berlin", disambiguate="earlier")
    ZonedDateTime("2023-02-26 01:30:00+02:00[Europe/Berlin]")
    
Or, you can even reject ambiguous datetimes altogether:
    
    >>> zoned = dt.assume_tz("Europe/Berlin", disambiguate="raise")
    
## 🌎 Overview¶
This page gives an overview of `whenever`’s main features for working with date and time. For more details, see the API reference.
### Core types¶
While the standard library has a single `datetime` type for all use cases, `whenever` provides distinct types similar to other modern datetime libraries [2]:
  * `Instant`—the simplest way to unambiguously represent a point on the timeline, also known as “exact time”. This type is analogous to a UNIX timestamp or UTC.
  * `PlainDateTime`—how humans represent time (e.g. “January 23rd, 2023, 3:30pm”), also known as “local time”. This type is analogous to an “naive” datetime in the standard library.
  * `ZonedDateTime`—A combination of the two concepts above: an exact time paired with a local time at a specific location. This type is analogous to an “aware” standard library datetime with `tzinfo` set to a `ZoneInfo` instance.


The distinction between these types is crucial for avoiding common pitfalls when working with dates and times. Read on to find out when to use each type.
Tip
If you prefer a video explanation, here is an excellent explanation of these concepts.
#### `Instant`¶
This is the simplest way to represent a moment on the timeline, independent of human complexities like timezones or calendars. An `Instant` maps 1:1 to UTC or a UNIX timestamp. It’s great for storing when something happened (or will happen) regardless of location.
    
    >>> livestream_starts = Instant.from_utc(2022, 10, 24, hour=17)
    Instant("2022-10-24 17:00:00Z")
    >>> Instant.now() > livestream_starts
    True
    >>> livestream_starts.add(hours=3).timestamp()
    1666641600
    
The value of this type is in its simplicity. It’s straightforward to compare, add, and subtract. It’s always clear what moment in time you’re referring to—without having to worry about timezones, Daylight Saving Time (DST), or the calendar.
See also
Why does Instant exist?
#### `PlainDateTime`¶
Humans typically represent time as a combination of date and time-of-day. For example: January 23rd, 2023, 3:30pm. While this information makes sense to people within a certain context, it doesn’t by itself refer to a moment on the timeline. This is because this date and time-of-day occur at different moments depending on whether you’re in Australia or Mexico, for example.
Another limitation is that you can’t account for Daylight Saving Time if you only have a date and time-of-day without a timezone. Therefore, it’s not possible to add exact time units to “plain” datetimes. This is because—strictly speaking—you don’t know what the local time will be in 3 hours: perhaps the clock will be moved forward or back due to Daylight Saving Time.
    
    >>> bus_departs = PlainDateTime(2020, 3, 14, hour=15)
    PlainDateTime("2020-03-14 15:00:00")
    # NOT possible:
    >>> Instant.now() > bus_departs                 # comparison with exact time
    >>> bus_departs.add(hours=3)                    # adding exact time units
    # IS possible:
    >>> PlainDateTime(2020, 3, 15) > bus_departs    # comparison with other plain datetimes
    >>> bus_departs.add(hours=3, ignore_dst=True)   # explicitly ignore DST
    >>> bus_departs.add(days=2)                     # calendar operations are OK
    
So how do you account for daylight saving time? Or find the corresponding exact time for a date and time-of-day? That’s what the next type is for.
#### `ZonedDateTime`¶
This is a combination of an exact and a local time at a specific location, with rules about Daylight Saving Time and other timezone changes.
    
    >>> bedtime = ZonedDateTime(2024, 3, 9, 22, tz="America/New_York")
    ZonedDateTime("2024-03-09 22:00:00-05:00[America/New_York]")
    # accounts for the DST transition overnight:
    >>> bedtime.add(hours=8)
    ZonedDateTime("2024-03-10 07:00:00-04:00[America/New_York]")
    
A timezone defines a UTC offset for each point on the timeline. As a result, any `Instant` can be converted to a `ZonedDateTime`. Converting from a `PlainDateTime`, however, may be ambiguous, because changes to the offset can result in local times occuring twice or not at all.
    
    >>> # Instant->Zoned is always straightforward
    >>> livestream_starts.to_tz("America/New_York")
    ZonedDateTime("2022-10-24 13:00:00-04:00[America/New_York]")
    >>> # Local->Zoned may be ambiguous
    >>> bus_departs.assume_tz("America/New_York")
    ZonedDateTime("2020-03-14 15:00:00-04:00[America/New_York]")
    
See also
Read about ambiguity in more detail here.
#### `OffsetDateTime`¶
> In API design, if you’ve got two things that are even subtly different, it’s worth having them as separate types—because you’re representing the meaning of your data more accurately.
> —Jon Skeet
Like `ZonedDateTime`, this type represents an exact time and a local time. The difference is that `OffsetDateTime` has a fixed offset from UTC rather than a timezone. As a result, it doesn’t know about Daylight Saving Time or other timezone changes.
Then why use it? Firstly, most datetime formats (e.g. ISO 8601 and RFC 2822) only have fixed offsets, making `OffsetDateTime` ideal for representing datetimes in these formats. Second, a `OffsetDateTime` is simpler—so long as you don’t need the ability to shift it. This makes `OffsetDateTime` an efficient and compatible choice for representing times in the past.
    
    >>> flight_departure = OffsetDateTime(2023, 4, 21, hour=9, offset=-4)
    >>> flight_arrival = OffsetDateTime(2023, 4, 21, hour=10, offset=-6)
    >>> (flight_arrival - flight_departure).in_hours()
    3
    >>> # but you CAN'T do this:
    >>> flight_arrival.add(hours=3)  # a DST-bug waiting to happen!
    >>> # instead:
    >>> flight_arrival.in_tz("America/New_York").add(hours=3)  # use the full timezone
    >>> flight_arrival.add(hours=3, ignore_dst=True)  # explicitly ignore DST
    
See also
  * Performing DST-safe arithmetic


#### Comparison of types¶
Here’s a summary of the differences between the types:
Instant
OffsetDT
ZonedDT
PlainDT  
knows the exact time
✅
✅
✅
❌  
knows the local time
❌
✅
✅
✅  
knows about DST rules [6]
❌
❌
✅
❌  
### Comparison and equality¶
All types support equality and comparison. However, `PlainDateTime` instances are never equal or comparable to the “exact” types.
#### Exact time¶
For exact types (`Instant`, `OffsetDateTime`, `ZonedDateTime`), comparison and equality are based on whether they represent the same moment in time. This means that two objects with different values can be equal:
    
    >>> # different ways of representing the same moment in time
    >>> inst = Instant.from_utc(2023, 12, 28, 11, 30)
    >>> as_5hr_offset = OffsetDateTime(2023, 12, 28, 16, 30, offset=5)
    >>> as_8hr_offset = OffsetDateTime(2023, 12, 28, 19, 30, offset=8)
    >>> in_nyc = ZonedDateTime(2023, 12, 28, 6, 30, tz="America/New_York")
    >>> # all equal
    >>> inst == as_5hr_offset == as_8hr_offset == in_nyc
    True
    >>> # comparison
    >>> in_nyc > OffsetDateTime(2023, 12, 28, 11, 30, offset=5)
    True
    
Note that if you want to compare for exact equality on the values (i.e. exactly the same year, month, day, hour, minute, etc.), you can use the `exact_eq()` method.
    
    >>> d = OffsetDateTime(2023, 12, 28, 11, 30, offset=5)
    >>> same = OffsetDateTime(2023, 12, 28, 11, 30, offset=5)
    >>> same_moment = OffsetDateTime(2023, 12, 28, 12, 30, offset=6)
    >>> d == same_moment
    True
    >>> d.exact_eq(same_moment)
    False
    >>> d.exact_eq(same)
    True
    
#### Local time¶
For `PlainDateTime`, equality is simply based on whether the values are the same, since there is no concept of timezones or UTC offset:
    
    >>> d = PlainDateTime(2023, 12, 28, 11, 30)
    >>> same = PlainDateTime(2023, 12, 28, 11, 30)
    >>> different = PlainDateTime(2023, 12, 28, 11, 31)
    >>> d == same
    True
    >>> d == different
    False
    
See also
See the documentation of `__eq__ (exact)` and `PlainDateTime.__eq__` for more details.
#### Strict equality¶
Local and exact types are never equal or comparable to each other. However, to comply with the Python data model, the equality operator won’t prevent you from using `==` to compare them. To prevent these mix-ups, use mypy’s `--strict-equality` flag.
    
    >>> # These are never equal, but Python won't stop you from comparing them.
    >>> # Mypy will catch this mix-up if you use enable --strict-equality flag.
    >>> Instant.from_utc(2023, 12, 28) == PlainDateTime(2023, 12, 28)
    False
    
Why not raise a TypeError?
It may seem like the equality operator should raise a `TypeError` in these cases, but this would result in surprising behavior when using values as dictionary keys.
Unfortunately, mypy’s `--strict-equality` is very strict, forcing you to match exact types exactly.
    
    x = Instant.from_utc(2023, 12, 28, 10)
    
    # mypy: ✅
    x == Instant.from_utc(2023, 12, 28, 10)
    
    # mypy: ❌ (too strict, this should be allowed)
    x == OffsetDateTime(2023, 12, 28, 11, offset=1)
    
To work around this, you can either convert explicitly:
    
    x == OffsetDateTime(2023, 12, 28, 11, offset=1).to_instant()
    
Or annotate with a union:
    
    x: OffsetDateTime | Instant == OffsetDateTime(2023, 12, 28, 11, offset=1)
    
### Conversion¶
#### Between exact types¶
You can convert between exact types with the `to_instant()`, `to_fixed_offset()`, `to_tz()`, and `to_system_tz()` methods. These methods return a new instance of the appropriate type, representing the same moment in time. This means the results will always compare equal to the original datetime.
    
    >>> d = ZonedDateTime(2023, 12, 28, 11, 30, tz="Europe/Amsterdam")
    >>> d.to_instant()  # The underlying moment in time
    Instant("2023-12-28 10:30:00Z")
    >>> d.to_fixed_offset(5)  # same moment with a +5:00 offset
    OffsetDateTime("2023-12-28 15:30:00+05:00")
    >>> d.to_tz("America/New_York")  # same moment in New York
    ZonedDateTime("2023-12-28 05:30:00-05:00[America/New_York]")
    >>> d.to_system_tz()  # same moment in the system timezone (e.g. Europe/Paris)
    ZonedDateTime("2023-12-28 11:30:00+01:00[Europe/Paris]")
    >>> d.to_fixed_offset(4) == d
    True  # always the same moment in time
    
#### To and from “plain” datetime¶
Conversion to a “plain” datetime is easy: calling `to_plain()` simply retrieves the date and time part of the datetime, and discards the any timezone or offset information.
    
    >>> d = ZonedDateTime(2023, 12, 28, 11, 30, tz="Europe/Amsterdam")
    >>> n = d.to_plain()
    PlainDateTime("2023-12-28 11:30:00")
    
You can convert from plain datetimes with the `assume_utc()`, `assume_fixed_offset()`, and `assume_tz()`, and `assume_system_tz()` methods.
    
    >>> n = PlainDateTime(2023, 12, 28, 11, 30)
    >>> n.assume_utc()
    Instant("2023-12-28 11:30:00Z")
    >>> n.assume_tz("Europe/Amsterdam")
    ZonedDateTime("2023-12-28 11:30:00+01:00[Europe/Amsterdam]")
    
Note
The seemingly inconsistent naming of the `to_*` and `assume_*` methods is intentional. The `assume_*` methods emphasize that the conversion is not self-evident, but based on assumptions of the developer.
### Ambiguity in timezones¶
Note
The API for handling ambiguity is largely inspired by that of Temporal, the redesigned date and time API for JavaScript.
In timezones, local clocks are often moved backwards and forwards due to Daylight Saving Time (DST) or political decisions. This makes it complicated to map a local time to a point on the timeline. Two common situations arise:
  * When the clock moves backwards, there is a period of time that repeats. For example, Sunday October 29th 2023 2:30am occurred twice in Paris. When you specify this time, you need to specify whether you want the earlier or later occurrence.
  * When the clock moves forwards, a period of time is skipped. For example, Sunday March 26th 2023 2:30am didn’t happen in Paris. When you specify this time, you need to specify how you want to handle this non-existent time. Common approaches are to extrapolate the time forward or backwards to 1:30am or 3:30am.
Note
You may wonder why skipped time is “extrapolated” like this, and not truncated. Why turn 2:30am into 3:30am and not cut it off at 1:59am when the gap occurs?
The reason for the “extrapolation” approach is:
    * It fits the most likely reason the time is skipped: we forgot to adjust the clock, or adjusted it too early
    * This is how other datetime libraries do it (e.g. JavaScript (Temporal), C# (Nodatime), Java, Python itself)
    * It corresponds with the iCalendar (RFC5545) standard of handling gaps
The figure in the Python docs here also shows how this “extrapolation” makes sense graphically.


`Whenever` allows you to customize how to handle these situations using the `disambiguate` argument:
`disambiguate`
Behavior in case of ambiguity  
`"raise"`
Raise `RepeatedTime` or `SkippedTime` exception.  
`"earlier"`
Choose the earlier of the two options  
`"later"`
Choose the later of the two options  
`"compatible"` (default)
Choose “earlier” for backward transitions and “later” for forward transitions. This matches the behavior of other established libraries, and the industry standard RFC 5545. It corresponds to setting `fold=0` in the standard library.  
    
    >>> paris = "Europe/Paris"
    
    >>> # Not ambiguous: everything is fine
    >>> ZonedDateTime(2023, 1, 1, tz=paris)
    ZonedDateTime("2023-01-01 00:00:00+01:00[Europe/Paris]")
    
    >>> # 1:30am occurs twice. Use 'raise' to reject ambiguous times.
    >>> ZonedDateTime(2023, 10, 29, 2, 30, tz=paris, disambiguate="raise")
    Traceback (most recent call last):
      ...
    whenever.RepeatedTime: 2023-10-29 02:30:00 is repeated in timezone Europe/Paris
    
    >>> # Explicitly choose the earlier option
    >>> ZonedDateTime(2023, 10, 29, 2, 30, tz=paris, disambiguate="earlier")
    ZoneDateTime(2023-10-29 02:30:00+01:00[Europe/Paris])
    
    >>> # 2:30am doesn't exist on this date (clocks moved forward)
    >>> ZonedDateTime(2023, 3, 26, 2, 30, tz=paris, disambiguate="raise")
    Traceback (most recent call last):
      ...
    whenever.SkippedTime: 2023-03-26 02:30:00 is skipped in timezone Europe/Paris
    
    >>> # Default behavior is compatible with other libraries and standards
    >>> ZonedDateTime(2023, 3, 26, 2, 30, tz=paris)
    ZonedDateTime("2023-03-26 03:30:00+02:00[Europe/Paris]")
    
### Arithmetic¶
Datetimes support various arithmetic operations.
#### Difference¶
You can get the duration between two datetimes or instants with the `-` operator or the `difference()` method. Exact and local types cannot be mixed, although exact types can be mixed with each other:
    
    >>> # difference in exact time
    >>> Instant.from_utc(2023, 12, 28, 11, 30) - ZonedDateTime(2023, 12, 28, tz="Europe/Amsterdam")
    TimeDelta(12:30:00)
    >>> # difference in local time
    >>> PlainDateTime(2023, 12, 28, 11).difference(
    ...     PlainDateTime(2023, 12, 27, 11),
    ...     ignore_dst=True
    ... )
    TimeDelta(24:00:00)
    
#### Units of time¶
You can add or subtract various units of time from a datetime instance.
    
    >>> d = ZonedDateTime(2023, 12, 28, 11, 30, tz="Europe/Amsterdam")
    >>> d.add(hours=5, minutes=30)
    ZonedDateTime("2023-12-28 17:00:00+01:00[Europe/Amsterdam]")
    
The behavior arithmetic behavior is different for three categories of units:
  1. Adding years and months may result in truncation of the date. For example, adding a month to August 31st results in September 31st, which isn’t valid. In such cases, the date is truncated to the last day of the month.
         >>> d = PlainDateTime(2023, 8, 31, hour=12)
         >>> d.add(months=1)
         PlainDateTime("2023-09-30 12:00:00")
         
Note
In case of dealing with `ZonedDateTime` there is a rare case where the resulting date might put the datetime in the middle of a DST transition. For this reason, adding years or months to these types accepts the `disambiguate` argument. By default, it tries to keep the same UTC offset, and if that’s not possible, it chooses the `"compatible"` option.
         >>> d = ZonedDateTime(2023, 9, 29, 2, 15, tz="Europe/Amsterdam")
         >>> d.add(months=1, disambiguate="raise")
         Traceback (most recent call last):
           ...
         whenever.RepeatedTime: 2023-10-29 02:15:00 is repeated in timezone 'Europe/Amsterdam'
         
  2. Adding days only affects the calendar date. Adding a day to a datetime will not affect the local time of day. This is usually same as adding 24 hours, except during DST transitions!
This behavior may seem strange at first, but it’s the most intuitive when you consider that you’d expect postponing a meeting “to tomorrow” should still keep the same time of day, regardless of DST changes. For this reason, this is the behavior of the industry standard RFC 5545 and other modern datetime libraries.
         >>> # on the eve of a DST transition
         >>> d = ZonedDateTime(2023, 3, 25, hour=12, tz="Europe/Amsterdam")
         >>> d.add(days=1)  # a day later, still 12 o'clock
         ZonedDateTime("2023-03-26 12:00:00+02:00[Europe/Amsterdam]")
         >>> d.add(hours=24)  # 24 hours later (we skipped an hour overnight!)
         ZonedDateTime("2023-03-26 13:00:00+02:00[Europe/Amsterdam]")
         
Note
As with months and years, adding days to a `ZonedDateTime` accepts the `disambiguate` argument, since the resulting date might put the datetime in a DST transition.
  3. Adding precise time units (hours, minutes, seconds) never results in ambiguity. If an hour is skipped or repeated due to a DST transition, precise time units will account for this.
         >>> d = ZonedDateTime(2023, 3, 25, hour=12, tz="Europe/Amsterdam")
         >>> d.add(hours=24)  # we skipped an hour overnight!
         ZonedDateTime("2023-03-26 13:00:00+02:00[Europe/Amsterdam]")
         


See also
Have a look at the documentation on deltas for more details on arithmetic operations, as well as more advanced features.
#### DST-safety¶
Date and time arithmetic can be tricky due to daylight saving time (DST) and other timezone changes. The API of the different classes is designed to avoid implicitly ignoring these. The type annotations and descriptive error messages should guide you to the correct usage.
  * `Instant` has no calendar, so it doesn’t support adding calendar units. Precise time units can be added without any complications.
  * `OffsetDateTime` has a fixed offset, so it cannot account for DST and other timezone changes. For example, the result of adding 24 hours to `2024-03-09 13:00:00-07:00` is different whether the offset corresponds to Denver or Phoenix. To perform DST-safe arithmetic, you should convert to a `ZonedDateTime` first. Or, if you don’t know the timezone and accept potentially incorrect results during DST transitions, pass `ignore_dst=True`.
        >>> d = OffsetDateTime(2024, 3, 9, 13, offset=-7)
        >>> d.add(hours=24)
        Traceback (most recent call last):
          ...
        ImplicitlyIgnoringDST: Adjusting a fixed offset datetime implicitly ignores DST [...]
        >>> d.to_tz("America/Denver").add(hours=24)
        ZonedDateTime("2024-03-10 14:00:00-06:00[America/Denver]")
        >>> d.add(hours=24, ignore_dst=True)  # NOT recommended
        OffsetDateTime("2024-03-10 13:00:00-07:00")
        
Attention
Even when working in a timezone without DST, you should still use `ZonedDateTime`. This is because political decisions in the future can also change the offset!
  * `ZonedDateTime` accounts for DST and other timezone changes, thus adding precise time units is always correct. Adding calendar units is also possible, but may result in ambiguity in rare cases, if the resulting datetime is in the middle of a DST transition:
        >>> d = ZonedDateTime(2024, 10, 3, 1, 15, tz="America/Denver")
        ZonedDateTime("2024-10-03 01:15:00-06:00[America/Denver]")
        >>> d.add(months=1)
        ZonedDateTime("2024-11-03 01:15:00-06:00[America/Denver]")
        >>> d.add(months=1, disambiguate="raise")
        Traceback (most recent call last):
          ...
        whenever.RepeatedTime: 2024-11-03 01:15:00 is repeated in timezone 'America/Denver'
        
  * `PlainDateTime` doesn’t have a timezone, so it can’t account for DST or other clock changes. Calendar units can be added without any complications, but, adding precise time units is only possible with explicit `ignore_dst=True`, because it doesn’t know about DST or other timezone changes:
        >>> d = PlainDateTime(2023, 10, 29, 1, 30)
        >>> d.add(hours=2)  # There could be a DST transition for all we know!
        Traceback (most recent call last):
          ...
        whenever.ImplicitlyIgnoringDST: Adjusting a plain datetime by time units
        ignores DST and other timezone changes. [...]
        >>> d.assume_tz("Europe/Amsterdam").add(hours=2)
        ZonedDateTime("2023-10-29 02:30:00+01:00[Europe/Amsterdam]")
        >>> d.add(hours=2, ignore_dst=True)  # NOT recommended
        PlainDateTime("2024-10-03 03:30:00")
        


Attention
Even when dealing with a timezone without DST, you should still use `ZonedDateTime` for precise time arithmetic. This is because political decisions in the future can also change the offset!
Here is a summary of the arithmetic features for each type:
Instant
OffsetDT
ZonedDT
LocalDT  
Difference
✅
✅
✅
⚠️ [3]  
add/subtract years, months, days
❌
⚠️ [3]
✅ [4]
✅  
add/subtract hours, minutes, seconds, …
✅
⚠️ [3]
✅
⚠️ [3]  
[3] (1,2,3,4)
Only possible by passing `ignore_dst=True` to the method.
[4]
The result by be ambiguous in rare cases. Accepts the `disambiguate` argument.
Why even have `ignore_dst`? Isn’t it dangerous?
While DST-safe arithmetic is certainly the way to go, there are cases where it’s simply not possible due to lack of information. Because there’s no way to to stop users from working around restrictions to get the result they want, `whenever` provides the `ignore_dst` option to at least make it explicit when this is happening.
#### Rounding¶
Note
The API for rounding is largely inspired by that of Temporal (JavaScript)
It’s often useful to truncate or round a datetime to a specific unit. For example, you might want to round a datetime to the nearest hour, or truncate it into 15-minute intervals.
The `round` method allows you to do this:
    
    >>> d = PlainDateTime(2023, 12, 28, 11, 32, 8)
    PlainDateTime("2023-12-28 11:32:08")
    >>> d.round("hour")
    PlainDateTime("2023-12-28 12:00:00")
    >>> d.round("minute", increment=15, mode="ceil")
    PlainDateTime("2023-12-28 11:45:00")
    
See the method documentation for more details on the available options.
### Formatting and parsing¶
`Whenever` supports formatting and parsing standardized formats
#### ISO 8601¶
The ISO 8601 standard is probably the format you’re most familiar with. What you may not know is that it’s a very complex standard with many options. Asking whether something “is proper ISO” is like asking whether something “is proper English”—there are many dialects and variations and people hold different opinions on what is “proper”.
Like all datetime libraries, `whenever` has to make some choices about which parts of the standard to support. `whenever` targets the most common and widely-used subset of the standard, while avoiding the more obscure and rarely-used parts, which are often the source of confusion and bugs.
Note
The ISO formats in `whenever` are designed so you can format and parse them without losing information. This makes it ideal for JSON serialization and other data interchange formats.
##### Parsing¶
`whenever`’s `parse_iso()` methods take mostly after Temporal, namely:
  * Both “extended” (e.g. `2023-12-28`) and “basic” (e.g. `20231228`) formats are supported.
  * Weekday and ordinal date formats are not supported: e.g. `2023-W52-5` or `2023-365`.
  * A space (`" "`) may be used instead of `T` to separate the date and time parts.
  * The date, time, and offset parts may independently choose to use extended or basic formats, so long as they are themselves consistent. e.g. `2023-12-28T113000+03` is OK, but `2023-1228T11:23` is not.
  * Characters may be lowercase or uppercase (e.g. `2023-12-28T11:30:00Z` is the same as `2023-12-28t11:30:00z`).
  * Only seconds may be fractional (e.g. `11:30:00.123456789Z` is OK but `11:30.5` is not).
  * Seconds may be precise up to 9 digits (nanoseconds).
  * Both `.` and `,` may be used as decimal separators
  * The offset `-00:00` is allowed, and is equivalent to `+00:00`
  * Offsets may be specified up to second-level precision (e.g. `2023-12-28T11:30:00+01:23:45`).
  * A IANA timezone identifier may be included in square brackets after the offset, like `2023-12-28T11:30:00+01[Europe/Paris]`. This is part of the recent RFC 9557 extension to ISO 8601.
  * In the duration format, the `W` unit may be used alongside other calendar units (`Y`, `M`, `D`).


##### Formatting¶
Below are the default string formats you get for calling each type’s `format_iso()` method:
Type
Default string format  
`Instant`
`YYYY-MM-DDTHH:MM:SSZ`  
`PlainDateTime`
`YYYY-MM-DDTHH:MM:SS`  
`ZonedDateTime`
`YYYY-MM-DDTHH:MM:SS±HH:MM[IANA TZ ID]` [1]  
`OffsetDateTime`
`YYYY-MM-DDTHH:MM:SS±HH:MM`  
Where applicable, the outputs can be customized using these parameters:
  * `unit` controls the smallest unit to include, ranging from `"hour"` to `"nanosecond"`. The default is `"auto"`, which includes full precision, but without trailing zeros:
        >>> i = Instant.now()
        >>> i.format_iso(unit="auto")
        '2025-09-28T21:24:17.664328Z'
        >>> d.format_iso(unit="minute")
        '2025-09-28T21:24Z'
        >>> d.format_iso(unit="nanosecond")
        '2025-09-28T21:24:17.664328000Z'  # fixed number of digits
        
  * `basic` controls whether to use the “basic” format (i.e. no date and time separators). By default, the extended format is used.
        >>> i.format_iso(basic=True)
        '20250928T212417.664328Z'
        >>> i.format_iso(basic=False)
        '2025-09-28T21:24:17.664328Z'
        
  * `sep` controls the separator between the date and time parts. T by default, but a space (`" "`) may be used instead. Other separators may be allowed in the future.
        >>> i.format_iso(sep=" ")
        '2025-09-28 21:24:17.664328Z'
        
  * `tz` controls whether to include the IANA timezone identifier in square brackets. Default is `"always"` which will raise an error if there is no timezone identifier (this may be the case for some system timezones). Use `"never"` to omit the timezone identifier, or `"auto"` to include it if available.
        >>> d = ZonedDateTime.now("Europe/Amsterdam")
        >>> d.format_iso(tz="auto")
        '2025-09-28T23:24:17.664328+02:00[Europe/Amsterdam]'
        >>> d.format_iso(tz="never")
        '2025-09-28T23:24:17.664328+02:00'
        


Why not support the full ISO 8601 spec?
The full ISO 8601 standard is not supported for several reasons:
  * It allows for a lot of rarely-used flexibility: e.g. fractional hours, week-based years, etc.
  * There are different versions of the standard with different rules
  * The full specification is not freely available


This isn’t a problem in practice since people referring to “ISO 8601” often mean the most common subset, which is what `whenever` supports. It’s rare for libraries to support the full standard.
If you do need to parse the full spectrum of ISO 8601, you can use a specialized library such as dateutil.parser.
#### RFC 2822¶
RFC 2822 is another common format for representing datetimes. It’s used in email headers and HTTP headers. The format is:
    
    Weekday, DD Mon YYYY HH:MM:SS ±HHMM
    
For example: `Tue, 13 Jul 2021 09:45:00 -0900`
Use the methods `format_rfc2822()` and `parse_rfc2822()` to format and parse to this format, respectively:
    
    >>> d = OffsetDateTime(2023, 12, 28, 11, 30, offset=+5)
    >>> d.format_rfc2822()
    'Thu, 28 Dec 2023 11:30:00 +0500'
    >>> OffsetDateTime.parse_rfc2822('Tue, 13 Jul 2021 09:45:00 -0900')
    OffsetDateTime("2021-07-13 09:45:00-09:00")
    
#### Custom formats¶
Future plans
Python’s builtin `strptime` has its limitations, so a more full-featured parsing API may be added in the future.
For now, basic customized parsing functionality is implemented in the `parse_strptime()` methods of `OffsetDateTime` and `PlainDateTime`. As the name suggests, these methods are thin wrappers around the standard library `strptime()` function. The same formatting rules apply.
    
    >>> OffsetDateTime.parse_strptime("2023-01-01+05:00", "%Y-%m-%d%z")
    OffsetDateTime("2023-01-01 00:00:00+05:00")
    >>> PlainDateTime.parse_strptime("2023-01-01 15:00", "%Y-%m-%d %H:%M")
    PlainDateTime("2023-01-01 15:00:00")
    
`ZonedDateTime` does not (yet) implement `parse_strptime()` methods, because they require disambiguation. If you’d like to parse into these types, use `PlainDateTime.parse_strptime()` to parse them, and then use the `assume_utc()`, `assume_fixed_offset()`, `assume_tz()`, or `assume_system_tz()` methods to convert them. This makes it explicit what information is being assumed.
    
    >>> d = PlainDateTime.parse_strptime("2023-10-29 02:30:00", "%Y-%m-%d %H:%M:%S")
    >>> d.assume_tz("Europe/Amsterdam")
    ZonedDateTime("2023-10-29 02:30:00+02:00[Europe/Amsterdam]")
    
#### Pydantic integration¶
Warning
Pydantic support is still in preview and may change in the future.
`Whenever` types support basic serialization and deserialization with Pydantic. The behavior is identical to the `parse_iso()` and `format_iso()` methods.
    
    >>> from pydantic import BaseModel
    >>> from whenever import ZonedDateTime, TimeDelta
    ...
    >>> class Event(BaseModel):
    ...     start: ZonedDateTime
    ...     duration: TimeDelta
    ...
    >>> event = Event(
    ...     start=ZonedDateTime(2023, 2, 23, hour=20, tz="Europe/Amsterdam"),
    ...     duration=TimeDelta(hours=2, minutes=30),
    ... )
    >>> d = event.model_dump_json()
    '{"start":"2023-02-23T20:00:00+01:00[Europe/Amsterdam]","duration":"PT2H30M"}'
    
Note
Whenever’s parsing is stricter then Pydantic’s default `datetime` parsing behavior. More flexible parsing may be added in the future.
### To and from the standard library¶
Each `whenever` datetime class can be converted to a standard library `datetime` with the `py_datetime()` method. Conversely, you can create instances from a standard library datetime with the `from_py_datetime()` classmethod.
    
    >>> from datetime import datetime, UTC
    >>> Instant.from_py_datetime(datetime(2023, 1, 1, tzinfo=UTC))
    Instant("2023-01-01 00:00:00Z")
    >>> ZonedDateTime(2023, 1, 1, tz="Europe/Amsterdam").py_datetime()
    datetime(2023, 1, 1, 0, 0, tzinfo=ZoneInfo('Europe/Amsterdam'))
    
Note
  * Converting to the standard library is not always lossless. Nanoseconds will be truncated to microseconds.
  * `from_py_datetime` also works for subclasses, so you can also ingest types from `pendulum` and `arrow` libraries.


### Date and time components¶
Aside from the datetimes themselves, `whenever` also provides `Date` for calendar dates and `Time` for representing times of day.
    
    >>> from whenever import Date, Time
    >>> Date(2023, 1, 1)
    Date("2023-01-01")
    >>> Time(12, 30)
    Time(12:30:00)
    
These types can be converted to datetimes and vice versa:
    
    >>> Date(2023, 1, 1).at(Time(12, 30))
    PlainDateTime("2023-01-01 12:30:00")
    >>> ZonedDateTime.now("Asia/Tokyo").date()
    Date("2023-07-13")
    
Dates support arithmetic with months and years, with similar semantics to modern datetime libraries:
    
    >>> d = Date(2023, 1, 31)
    >>> d.add(months=1)
    Date("2023-02-28")
    >>> d - Date(2022, 10, 15)
    DateDelta("P3M16D")
    
There’s also `YearMonth` and `MonthDay` for representing year-month and month-day combinations, respectively. These are useful for representing recurring events or birthdays.
See the API reference for more details.
### Testing¶
#### Patching the current time¶
Sometimes you need to ‘fake’ the output of `.now()` functions, typically for testing. `Whenever` supports various ways to do this, depending on your needs:
  1. With `whenever.patch_current_time`. This patcher only affects `whenever`, not the standard library or other libraries. See its documentation for more details.
  2. With the time-machine package. Using `time-machine` does affect the standard library and other libraries, which can lead to unintended side effects. Note that `time-machine` doesn’t support PyPy.


Note
It’s also possible to use the freezegun library, but it will only work on the Pure-Python version of `whenever`.
Tip
Instead of relying on patching, consider using dependency injection instead. This is less error-prone and more explicit.
You can do this by adding `now` argument to your function, like this:
    
    def greet(name, now=Instant.now):
        current_time = now()
        # more code here...
    
    # in normal use, you don't notice the difference:
    greet('bob')
    
    # to test it, pass a custom function:
    greet('alice', now=lambda: Instant.from_utc(2023, 1, 1))
    
#### Patching the system timezone¶
For changing the system timezone in tests, set the TZ environment variable and use the `reset_system_tz()` helper function to update the timezone cache. Do note that this function only affects whenever, and not the standard library’s behavior.
Below is an example of a testing helper that can be used with `pytest`:
    
    import os
    import pytest
    from contextlib import contextmanager
    from unittest.mock import patch
    from whenever import reset_system_tz
    
    @contextmanager
    def system_tz_ams():
        try:
            with patch.dict(os.environ, {"TZ": "Europe/Amsterdam"}):
                reset_system_tz()  # update the timezone cache
                yield
        finally:
            reset_system_tz()  # don't forget to set the old timezone back!
    
### The system timezone¶
The system timezone is the timezone that your operating system is set to. You can create datetimes in the system timezone by using the `assume_system_tz()` or `to_system_tz()` methods:
    
    >>> from whenever import PlainDateTime, Instant
    >>> plain = PlainDateTime(2020, 8, 15, hour=8)
    >>> d = plain.assume_system_tz()
    ZonedDateTime("2020-08-15 08:00:00-04:00[America/New_York]")
    >>> Instant.now().to_system_tz()
    ZonedDateTime("2023-12-28 11:30:00-05:00[America/New_York]")
    
When working with the timezone of the current system, there are a few things to keep in mind.
#### System timezone changes¶
It’s important to be aware that the system timezone can change. `whenever` caches the system timezone at time you access it first. This ensures predictable and fast behavior.
In the rare case that you need to change the system timezone while your program is running, you can use the `reset_system_tz()` method to determine the system timezone again. Existing datetimes will not be affected by this change, but new datetimes will use the updated system timezone.
    
    >>> # initialization where the system timezone is America/New_York
    >>> plain = PlainDateTime(2020, 8, 15, hour=8)
    >>> d = plain.assume_system_tz()
    ZonedDateTime("2020-08-15 08:00:00-04:00[America/New_York]")
    ...
    >>> # we change the system timezone to Amsterdam
    >>> os.environ["TZ"] = "Europe/Amsterdam"
    >>> whenever.reset_system_tz()
    ...
    >>> d  # existing objects remain unchanged
    ZonedDateTime("2020-08-15 08:00:00-04:00[America/New_York]")
    >>> # new objects will use the new system timezone
    >>> Instant.now().to_system_tz()
    ZonedDateTime("2025-08-15 15:03:28+01:00[Europe/Amsterdam]")
    
#### Non-IANA system timezones¶
While most system timezones can be matched with a IANA timezone ID (like `Europe/Amsterdam`), some systems use custom timezone definitions that don’t (unambiguously) map to a IANA timezone ID. For example, some systems may set the `TZ` environment variable to a POSIX TZ string like `CET-1CEST,M3.5.0,M10.5.0/3`, or specify a custom timezone file.
    
    >>> os.environ["TZ"] = "CET-1CEST,M3.5.0,M10.5.0/3"
    >>> whenever.reset_system_tz()
    
These type of timezone definitions can still account for Daylight Saving Time (DST) and other timezone changes:
    
    >>> d = plain.assume_system_tz()
    ZonedDateTime("2024-06-04 12:00:00+02:00[<system timezone without ID>]")
    >>> # Correct UTC offset after adding 5 months
    >>> d.add(months=5)
    ZonedDateTime("2024-11-04 12:00:00+01:00[<system timezone without ID>]")
    
However there are some limitations of such instances of `ZonedDateTime`:
  1. Their `tz` attribute is `None`
  2. They cannot be pickled
  3. Their ISO 8601 string representation does not include a IANA timezone ID
  4. The result of `py_datetime()` will have a fixed offset, not a `ZoneInfo` object.

[1]
The timezone ID is not part of the core ISO 8601 standard, but is part of the RFC 9557 extension. This format is commonly used by datetime libraries in other languages as well.
[2]
java.time, Noda Time (C#), and partly Temporal (JavaScript) all use a similar datamodel.
[6]
Daylight Saving Time isn’t the only reason for UTC offset changes. Changes can also occur due to political decisions, or historical reasons.
## ⏳ Deltas¶
As we’ve seen earlier, you can add and subtract time units from datetimes:
    
    >>> dt.add(hours=5, minutes=30)
    
However, sometimes you want to operate on these durations directly. For example, you might want to reuse a particular duration, or perform arithmetic on it. For this, whenever provides an API designed to help you avoid common pitfalls. The type annotations and descriptive errors should guide you to the correct usage.
Durations are created using the duration units provided. Here is a quick demo:
    
    >>> from whenever import years, months, days, hours, minutes
    >>> # Precise units create a TimeDelta, supporting broad arithmetic
    >>> movie_runtime = hours(2) + minutes(9)
    TimeDelta(02:09:00)
    >>> movie_runtime.in_minutes()
    129.0
    >>> movie_runtime / 1.2  # what if we watch it at 1.2x speed?
    TimeDelta(01:47:30)
    ...
    >>> # Calendar units create a DateDelta, with more limited arithmetic
    >>> project_estimate = months(1) + days(10)
    DateDelta("P1M10D")
    >>> Date(2023, 1, 29) + project_estimate
    Date("2023-03-10")
    >>> project_estimate * 2  # make it pessimistic
    DateDelta("P2M20D")
    ...
    >>> # Mixing date and time units creates a generic DateTimeDelta
    >>> project_estimate + movie_runtime
    DateTimeDelta("P1M10DT2H9M")
    ...
    >>> # API ensures common mistakes are caught early:
    >>> project_estimate * 1.3             # Impossible arithmetic on calendar units
    >>> project_estimate.in_hours()        # Resolving calendar units without context
    >>> Date(2023, 1, 29) + movie_runtime  # Adding time to a date
    
### Types of deltas¶
There are three duration types in whenever:
  * `TimeDelta`, created by precise units `hours()`, `minutes()`, `seconds()`, and `microseconds()`. Their duration is always the same and independent of the calendar. Arithmetic on time units is straightforward. It behaves similarly to the `timedelta` of the standard library.
  * `DateDelta`, created by the calendar units `years()`, `months()`, `weeks()`, and `days()`. They don’t have a precise duration, as this depends on the context. For example, the number of days in a month varies, and a day may be longer or shorter than 24 hours due to Daylight Saving Time. This makes arithmetic on calendar units less intuitive.
  * `DateTimeDelta`, created when you mix time and calendar units.


This distinction determines which operations are supported:
Feature
`TimeDelta`
`DateDelta`
`DateTimeDelta`  
Add to datetimes
See here  
Add to `Date`
❌
✅
❌  
division (÷)
✅
❌
❌  
multiplication (×)
✅
⚠️ [1]
⚠️ [1]  
comparison (`>, >=, <, <=`)
✅
❌
❌  
Commutative: `dt + a + b == dt + b + a`
✅
❌
❌  
Reversible: `(dt + a) - a == dt`
✅
❌
❌  
normalized
✅
⚠️ [2]
⚠️ [2]  
[1] (1,2)
Only by integers
[2] (1,2)
Years/months and weeks/days are normalized amongst each other, but not with other units.
### Multiplication¶
You can multiply time units by a number:
    
    >>> 1.5 * hours(2)
    TimeDelta(03:00:00)
    
Date units can only be multiplied by integers. “1.3 months” isn’t a well-defined concept, so it’s not supported:
    
    >>> months(3) * 2
    DateDelta("P6M")
    
### Division¶
Only time units can be divided:
    
    >>> hours(3) / 1.5
    TimeDelta(02:00:00)
    
Date units can’t be divided. “A year divided by 11.2”, for example, can’t be defined.
### Commutativity¶
The result of adding two time durations is the same, regardless of what order you add them in:
    
    >>> dt = Instant.from_utc(2020, 1, 29)
    >>> dt + hours(2) + minutes(30)
    Instant("2020-01-29 02:30:00Z")
    >>> dt + minutes(30) + hours(2)  # same result
    
This is not the case for date units. The result of adding two date units depends on the order:
    
    >>> d = Date(2020, 1, 29)
    >>> d + months(1) + days(3)
    Date("2020-03-03")
    >>> d + days(3) + months(1)
    Date("2020-03-01")
    
### Reversibility¶
Adding a time duration and then subtracting it again gives you the original datetime:
    
    >>> dt + hours(3) - hours(3) == dt
    True
    
This is not the case for date units:
    
    >>> jan30 = Date(2020, 1, 30)
    >>> jan30 + months(1)
    Date("2020-02-29")
    >>> jan30 + months(1) - months(1)
    Date("2020-01-29")
    
### Comparison¶
You can compare time durations:
    
    >>> hours(3) > minutes(30)
    True
    
This is not the case for date units:
    
    >>> months(1) > days(30)  # no universal answer
    
### Normalization¶
Time durations are always fully normalized: hours, minutes, seconds, milliseconds, microseconds, and nanoseconds all roll over into each other:
    
    >>> minutes(70)
    TimeDelta(01:10:00)
    
Only some date units can be normalized: years and months are normalized amongst each other, and weeks and days are normalized amongst each other. 1 year doesn’t always correspond to a fixed number of days, but it does always correspond to 12 months. One day also doesn’t correspond to a fixed number of hours, as this can change depending on Daylight Saving Time, for example.
    
    >>> months(13)
    DateDelta("P1Y1M")
    >>> months(1) + weeks(4)
    DateDelta("P1M28D")
    >>> days(1) + hours(24)
    DateTimeDelta("P1DT24H")
    
### Equality¶
Two time durations are equal if their sum of components is equal:
    
    >>> hours(1) + minutes(30) == hours(2) - minutes(30)
    True
    
Since date units are only partially normalized, date durations are only equal if months/years and weeks/days are equal amongst each other:
    
    >>> months(1) == days(31)
    False  # a month will never equal a fixed number of days
    >>> years(1) + weeks(1) == months(12) + days(7)
    True  # a years is always 12 months, and a week is always 7 days
    
### ISO 8601 format¶
The ISO 8601 standard defines formats for specifying durations, the most common being:
    
    ±PnYnMnDTnHnMnS
    
Where:
  * `P` is the period designator, and `T` separates date and time components.
  * `nY` is the number of years, `nM` is the number of months, etc.
  * Only seconds may have a fractional part.


For example:
  * `P3Y4DT12H30M` is 3 years, 4 days, 12 hours, and 30 minutes.
  * `-P2M5D` is -2 months, and -5 days.
  * `P0D` is zero.
  * `+PT5M4.25S` is 5 minutes and 4.25 seconds.


All deltas can be converted to and from this format using the methods `format_iso()` and `parse_iso()`.
    
    >>> hours(3).format_iso()
    'PT3H'
    >>> (-years(1) - months(3) - minutes(30.25)).format_iso()
    '-P1Y3MT30M15S'
    >>> DateDelta.parse_iso('-P2M')
    DateDelta(-2M)
    >>> DateTimeDelta.parse_iso('P3YT90M')
    DateTimeDelta("P3YT1H30M")
    
Attention
Full conformance to the ISO 8601 standard is not provided, because:
  * It allows for a lot of unnecessary flexibility (e.g. fractional components other than seconds)
  * There are different revisions with different rules
  * The full specification is not freely available


Supporting a commonly used subset is more practical. This is also what established libraries such as java.time and Nodatime do.
## ❓ FAQ¶
### Does performance really matter for a datetime library?¶
Most of the time, datetime handling isn’t the main bottleneck in Python programs—but then again, very few things are. Still, datetime logic is arithmetic-heavy and often applied in bulk, making it a classic case where faster code pays off. That’s why many core Python components are backed by optimized implementations, and why this library offers a Rust version for speed alongside a pure-Python version for portability.
### Why does `Instant` exist?¶
Since you can also express a moment in time using `ZonedDateTime` you might wonder why `Instant` exists. The reason it exists is precisely because it doesn’t include a timezone. By using `Instant`, you clearly express that you only care about when something happened, and don’t care about the local time.
Consider the difference in intent between these two classes:
    
    class ChatMessage:
        sent: Instant
        content: str
    
    
    class ChatMessage:
        sent: ZonedDateTime
        content: str
    
In the first example, it’s clear that you only care about the moment when chat messages were sent. In the second, you communicate that you also store the user’s local time. This intent is crucial for reasoning about the code, and extending it correctly (e.g. with migrations, API endpoints, etc).
### Why the name `PlainDateTime`?¶
This has been an oft-discussed topic. Several names were considered for the concept of a “datetime without a timezone”.
Each option had its pros and cons.
  * Why not `NaiveDateTime`? This name is already used in the standard library, which does give it recognition. However, “naive” is a decidedly negative term. While datetimes without a timezone can be used in a naive way by developers who don’t understand the implications, they are not inherently wrong to use.
  * Why not `CivilDateTime`? This is the most “technically correct” name, as it refers to the time as used in civilian life. This name is most notably used in Jiff (Rust) and Abseil (C++) libraries. While this niche name is a boon to these langauges, Python tends to favor more common, non-jargon names: “dict” over “hashmap”, “list” over “array”, etc.
  * Why not `LocalDateTime`? This is the name that ISO8601 gives to the concept, also making it a “technically correct” name. However, the term “local” has become overloaded in the Python world where it often refers to the system timezone.


While `PlainDateTime` is not perfect, it has the following advantages:
  * Javascript’s new Temporal API uses this name. There’s significant overlap between Python and Javascript developers, so this name is likely to be familiar as its popularity grows.
  * It’s a name that is easy to understand and remember, also for non-native speakers.


Common critiques of `PlainDateTime` are:
  * The name doesn’t convey any meaning in itself. This is also a strength. It is simply a date+time. Yes, it can be used to represent a local time, but it doesn’t have to be.
  * The name is defined by what it is not. Actually, it’s really common to name things in opposition to something else. Think of: “stainless steel”, “plain text”, or “serverless computing”.


### Are leap seconds supported?¶
Leap seconds are unsupported. Taking leap seconds into account is a complex and niche feature, which is not needed for the vast majority of applications. This decision is consistent with other modern libraries (e.g. NodaTime, Temporal) and standards (RFC 5545, Unix time) which do not support leap seconds.
One improvement that is planned: allowing the parsing of leap seconds, which are then truncated to 59 seconds.
### Why no drop-in replacement for `datetime`?¶
Fixing the issues with the standard library requires a different API. Keeping the same API would mean that the same issues would remain. Also, inheriting from the standard library would result in brittle code: many popular libraries expect `datetime` exactly, and don’t work with subclasses.
### Is it production-ready?¶
The core functionality is complete and mostly stable. The goal is to reach 1.0 soon, but the API may change 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!
### Where do the benchmarks come from?¶
More information about the benchmarks can be found in the `benchmarks` directory of the repository.
### How can I use the pure-Python version?¶
Whenever is implemented both in Rust and in pure Python. By default, the Rust extension is used, as it’s faster and more memory-efficient. But you can opt out of it if you prefer the pure-Python version, which has a smaller disk footprint and works on all platforms.
Note
On PyPy and GraalVM, the Python implementation is automatically used. No need to configure anything.
To opt out of the Rust extension and use the pure-Python version, install from the source distribution with the `WHENEVER_NO_BUILD_RUST_EXT` environment variable set:
    
    WHENEVER_NO_BUILD_RUST_EXT=1 pip install whenever --no-binary whenever
    
You can check if the Rust extension is being used by running:
    
    python -c "import whenever; print(whenever._EXTENSION_LOADED)"
    
Note
If you’re using Poetry or another third-party package manager, you should consult its documentation on opting out of binary wheels.
### What about `dateutil`?¶
I haven’t included it in the comparison since dateutil is more of an extension to datetime, while whenever (and Pendulum and Arrow) are more like replacements.
That said, here are my thoughts on dateutil: while it certainly provides useful helpers (especially for parsing and arithmetic), it doesn’t solve the (IMHO) most glaring issues with the standard library: DST-safety and typing for naive/aware. These are issues that only a full replacement can solve.
### Why not simply wrap Rust’s `jiff` library?¶
Jiff is a modern datetime library in Rust with similar goals and inspiration as whenever. There are several reasons that whenever doesn’t simply wrap jiff though:
  1. Jiff didn’t exist when whenever was created. Wrapping jiff was only an option after most of the functionality was already implemented.
  2. In order to provide a pure-Python version of whenever, jiff’s logic would need to be re-implemented in Python–and kept in sync.
  3. Jiff has a slightly different design philosophy, most notably de-emphasizing the difference between offset and zoned datetimes.
  4. Jiff can’t make use of Python’s bundled timezone database (tzdata) if present.
  5. Writing a rust library with Python bindings primarily in mind allows for some optimizations.


If you’re interested in a straightforward wrapper around jiff, check out Ry.
### Why can’t I subclass whenever classes?¶
Whenever classes aren’t meant to be subclassed. There’s no plan to change this due to the following reasons:
  1. The benefits of subclassing are limited. If you want to extend the classes, composition is a better way to do it. Alternatively, you can use Python’s dynamic features to create something that behaves like a subclass.
  2. For a class to support subclassing properly, a lot of extra work is needed. It also adds many subtle ways to misuse the API, that are hard to control.
  3. Enabling subclassing would undo some performance optimizations.


## 📖 API reference¶
All classes are immutable.
### Datetimes¶
Below is a simplified overview of the datetime classes and how they relate to each other.
#### Common behavior¶
The following base classes encapsulate common behavior. They are not meant to be used directly.
class whenever._BasicConversions¶
    
Methods for types converting to/from the standard library and ISO8601:
  * `Instant`
  * `PlainDateTime`
  * `ZonedDateTime`
  * `OffsetDateTime`


(This base class class itself is not for public use.)
abstract classmethod from_py_datetime(d: datetime, /) → _T¶
    
Create an instance from a `datetime` object. Inverse of `py_datetime()`.
Note
The datetime is checked for validity, raising similar exceptions to the constructor. `ValueError` is raised if the datetime doesn’t have the correct tzinfo matching the class. For example, `ZonedDateTime` requires a `ZoneInfo` tzinfo.
Warning
No exceptions are raised if the datetime is ambiguous. Its `fold` attribute is used to disambiguate.
py_datetime() → datetime¶
    
Convert to a standard library `datetime`
Note
  * Nanoseconds are truncated to microseconds. If you wish to customize the rounding behavior, use the `round()` method first.
  * In case of a ZonedDateTime linked to a system timezone without a IANA timezone ID, the returned Python datetime will have a fixed offset (`timezone` tzinfo)


abstract format_iso() → str¶
    
Format an ISO8601 string representation. Each subclass has a different format.
Where applicable, keyword arguments `unit`, `basic`, `sep`, and `tz` are supported to customize the output.
See here for more information.
abstract classmethod parse_iso(s: str, /) → _T¶
    
Create an instance from an ISO 8601 representation, which is different for each subclass.
See here for more information.
class whenever._ExactTime¶
    
Bases: `_BasicConversions`
Methods for types that represent a specific moment in time.
Implemented by:
  * `Instant`
  * `ZonedDateTime`
  * `OffsetDateTime`


(This base class class itself is not for public use.)
__eq__(other: object) → bool¶
    
Check if two datetimes represent at the same moment in time
`a == b` is equivalent to `a.to_instant() == b.to_instant()`
Note
If you want to exactly compare the values on their values instead, use `exact_eq()`.
Example
    
    >>> Instant.from_utc(2020, 8, 15, hour=23) == Instant.from_utc(2020, 8, 15, hour=23)
    True
    >>> OffsetDateTime(2020, 8, 15, hour=23, offset=1) == (
    ...     ZonedDateTime(2020, 8, 15, hour=18, tz="America/New_York")
    ... )
    True
    
__ge__(other: _ExactTime) → bool¶
    
Compare two datetimes by when they occur in time
`a >= b` is equivalent to `a.to_instant() >= b.to_instant()`
Example
    
    >>> OffsetDateTime(2020, 8, 15, hour=19, offset=-8) >= (
    ...     ZoneDateTime(2020, 8, 15, hour=20, tz="Europe/Amsterdam")
    ... )
    True
    
__gt__(other: _ExactTime) → bool¶
    
Compare two datetimes by when they occur in time
`a > b` is equivalent to `a.to_instant() > b.to_instant()`
Example
    
    >>> OffsetDateTime(2020, 8, 15, hour=19, offset=-8) > (
    ...     ZoneDateTime(2020, 8, 15, hour=20, tz="Europe/Amsterdam")
    ... )
    True
    
__le__(other: _ExactTime) → bool¶
    
Compare two datetimes by when they occur in time
`a <= b` is equivalent to `a.to_instant() <= b.to_instant()`
Example
    
    >>> OffsetDateTime(2020, 8, 15, hour=23, offset=8) <= (
    ...     ZoneDateTime(2020, 8, 15, hour=20, tz="Europe/Amsterdam")
    ... )
    True
    
__lt__(other: _ExactTime) → bool¶
    
Compare two datetimes by when they occur in time
`a < b` is equivalent to `a.to_instant() < b.to_instant()`
Example
    
    >>> OffsetDateTime(2020, 8, 15, hour=23, offset=8) < (
    ...     ZoneDateTime(2020, 8, 15, hour=20, tz="Europe/Amsterdam")
    ... )
    True
    
abstract __sub__(other: _ExactTime) → TimeDelta¶
    
Calculate the duration between two datetimes
`a - b` is equivalent to `a.to_instant() - b.to_instant()`
Equivalent to `difference()`.
See the docs on arithmetic for more information.
Example
    
    >>> d = Instant.from_utc(2020, 8, 15, hour=23)
    >>> d - ZonedDateTime(2020, 8, 15, hour=20, tz="Europe/Amsterdam")
    TimeDelta(05:00:00)
    
difference(other: Instant | OffsetDateTime | ZonedDateTime, /) → TimeDelta¶
    
Calculate the difference between two instants in time.
Equivalent to `__sub__()`.
See the docs on arithmetic for more information.
exact_eq(other: _T, /) → bool¶
    
Compare objects by their values (instead of whether they represent the same instant). Different types are never equal.
Note
If `a.exact_eq(b)` is true, then `a == b` is also true, but the converse is not necessarily true.
Examples
    
    >>> a = OffsetDateTime(2020, 8, 15, hour=12, offset=1)
    >>> b = OffsetDateTime(2020, 8, 15, hour=13, offset=2)
    >>> a == b
    True  # equivalent instants
    >>> a.exact_eq(b)
    False  # different values (hour and offset)
    >>> a.exact_eq(Instant.now())
    TypeError  # different types
    
classmethod from_timestamp(i: int | float, /, **kwargs) → _T¶
    
Create an instance from a UNIX timestamp. The inverse of `timestamp()`.
`ZonedDateTime` and `OffsetDateTime` require a `tz=` and `offset=` kwarg, respectively.
Note
`from_timestamp()` also accepts floats, in order to ease migration from the standard library. Note however that `timestamp()` only returns integers. The reason is that floating point timestamps are not precise enough to represent all instants to nanosecond precision.
Example
    
    >>> Instant.from_timestamp(0)
    Instant("1970-01-01T00:00:00Z")
    >>> ZonedDateTime.from_timestamp(1_123_000_000, tz="America/New_York")
    ZonedDateTime("2005-08-02 12:26:40-04:00[America/New_York]")
    
classmethod from_timestamp_millis(i: int, /, **kwargs) → _T¶
    
Like `from_timestamp()`, but for milliseconds.
classmethod from_timestamp_nanos(i: int, /, **kwargs) → _T¶
    
Like `from_timestamp()`, but for nanoseconds.
classmethod now(**kwargs) → _T¶
    
Create an instance from the current time.
This method on `ZonedDateTime` and `OffsetDateTime` requires an additional timezone or offset argument, respectively.
Example
    
    >>> Instant.now()
    Instant("2021-08-15T22:12:00.49821Z")
    >>> ZonedDateTime.now("Europe/London")
    ZonedDateTime("2021-08-15 23:12:00.50332+01:00[Europe/London]")
    
timestamp() → int¶
    
The UNIX timestamp for this datetime. Inverse of `from_timestamp()`.
Note
In contrast to the standard library, this method always returns an integer, not a float. This is because floating point timestamps are not precise enough to represent all instants to nanosecond precision. This decision is consistent with other modern date-time libraries.
Example
    
    >>> Instant.from_utc(1970, 1, 1).timestamp()
    0
    >>> ts = 1_123_000_000
    >>> Instant.from_timestamp(ts).timestamp() == ts
    True
    
timestamp_millis() → int¶
    
Like `timestamp()`, but with millisecond precision.
timestamp_nanos() → int¶
    
Like `timestamp()`, but with nanosecond precision.
to_fixed_offset(offset: int | TimeDelta | None = None, /) → OffsetDateTime¶
    
Convert to an OffsetDateTime that represents the same moment in time.
If not offset is given, the offset is taken from the original datetime.
to_system_tz() → ZonedDateTime¶
    
Convert to a ZonedDateTime of the system’s timezone.
to_tz(tz: str, /) → ZonedDateTime¶
    
Convert to a ZonedDateTime that represents the same moment in time.
Raises:
    
TimeZoneNotFoundError – If the timezone ID is not found in the timezone database.
class whenever._LocalTime¶
    
Bases: `_BasicConversions`, `ABC`
Methods for types that know a local date and time-of-day:
  * `PlainDateTime`
  * `ZonedDateTime`
  * `OffsetDateTime`


(The class itself is not for public use.)
abstract add(*, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0, hours: float = 0, minutes: float = 0, seconds: float = 0, milliseconds: float = 0, microseconds: float = 0, nanoseconds: int = 0, **kwargs) → _T¶
    
Add date and time units to this datetime.
Arithmetic on datetimes is complicated. Additional keyword arguments `ignore_dst` and `disambiguate` may be relevant for certain types and situations. See the docs on arithmetic for more information and the reasoning behind it.
date() → Date¶
    
The date part of the datetime
Example
    
    >>> d = Instant.from_utc(2021, 1, 2, 3, 4, 5)
    >>> d.date()
    Date("2021-01-02")
    
To perform the inverse, use `Date.at()` and a method like `assume_utc()` ortestoffset `assume_tz()`:
    
    >>> date.at(time).assume_tz("Europe/London")
    
property day: int¶
    
property hour: int¶
    
property minute: int¶
    
property month: int¶
    
property nanosecond: int¶
    
abstract replace(**kwargs: Any) → _T¶
    
Construct a new instance with the given fields replaced.
Arguments are the same as the constructor, but only keyword arguments are allowed.
Note
If you need to shift the datetime by a duration, use the addition and subtraction operators instead. These account for daylight saving time and other complications.
Warning
The same exceptions as the constructor may be raised. For system and zoned datetimes, The `disambiguate` keyword argument is recommended to resolve ambiguities explicitly. For more information, see whenever.rtfd.io/en/latest/overview.html#ambiguity-in-timezones
Example
    
    >>> d = PlainDateTime(2020, 8, 15, 23, 12)
    >>> d.replace(year=2021)
    PlainDateTime("2021-08-15 23:12:00")
    >>>
    >>> z = ZonedDateTime(2020, 8, 15, 23, 12, tz="Europe/London")
    >>> z.replace(year=2021)
    ZonedDateTime("2021-08-15T23:12:00+01:00")
    
replace_date(date: Date, /, **kwargs) → _T¶
    
Create a new instance with the date replaced
Example
    
    >>> d = PlainDateTime(2020, 8, 15, hour=4)
    >>> d.replace_date(Date(2021, 1, 1))
    PlainDateTime("2021-01-01T04:00:00")
    >>> zdt = ZonedDateTime.now("Europe/London")
    >>> zdt.replace_date(Date(2021, 1, 1))
    ZonedDateTime("2021-01-01T13:00:00.23439+00:00[Europe/London]")
    
See `replace()` for more information.
replace_time(time: Time, /, **kwargs) → _T¶
    
Create a new instance with the time replaced
Example
    
    >>> d = PlainDateTime(2020, 8, 15, hour=4)
    >>> d.replace_time(Time(12, 30))
    PlainDateTime("2020-08-15T12:30:00")
    >>> zdt = ZonedDateTime.now("Europe/London")
    >>> zdt.replace_time(Time(12, 30))
    ZonedDateTime("2024-06-15T12:30:00+01:00[Europe/London]")
    
See `replace()` for more information.
round(unit: Literal['day', 'hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'] = 'second', increment: int = 1, mode: Literal['ceil', 'floor', 'half_ceil', 'half_floor', 'half_even'] = 'half_even') → _T¶
    
Round the datetime to the specified unit and increment. Different rounding modes are available.
Examples
    
    >>> d = ZonedDateTime(2020, 8, 15, 23, 24, 18, tz="Europe/Paris")
    >>> d.round("day")
    ZonedDateTime("2020-08-16 00:00:00+02:00[Europe/Paris]")
    >>> d.round("minute", increment=15, mode="floor")
    ZonedDateTime("2020-08-15 23:15:00+02:00[Europe/Paris]")
    
Notes
  * In the rare case that rounding results in an ambiguous time, the offset is preserved if possible. Otherwise, the time is resolved according to the “compatible” strategy.
  * Rounding in “day” mode may be affected by DST transitions. i.e. on 23-hour days, 11:31 AM is rounded up.
  * For `OffsetDateTime`, the `ignore_dst` parameter is required, because it is possible (though unlikely) that the rounded datetime will not have the same offset.
  * This method has similar behavior to the `round()` method of Temporal objects in JavaScript.


property second: int¶
    
abstract subtract(*, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0, hours: float = 0, minutes: float = 0, seconds: float = 0, milliseconds: float = 0, microseconds: float = 0, nanoseconds: int = 0, **kwargs) → _T¶
    
Inverse of `add()`.
time() → Time¶
    
The time-of-day part of the datetime
Example
    
    >>> d = ZonedDateTime(2021, 1, 2, 3, 4, 5, tz="Europe/Paris")
    ZonedDateTime("2021-01-02T03:04:05+01:00[Europe/Paris]")
    >>> d.time()
    Time(03:04:05)
    
To perform the inverse, use `Time.on()` and a method like `assume_utc()` or `assume_tz()`:
    
    >>> time.on(date).assume_tz("Europe/Paris")
    
property year: int¶
    
class whenever._ExactAndLocalTime¶
    
Bases: `_LocalTime`, `_ExactTime`
Common behavior for all types that know an exact time and corresponding local date and time-of-day.
  * `ZonedDateTime`
  * `OffsetDateTime`


(The class itself it not for public use.)
property offset: TimeDelta¶
    
The UTC offset of the datetime
to_instant() → Instant¶
    
Get the underlying instant in time
Example
    
    >>> d = ZonedDateTime(2020, 8, 15, hour=23, tz="Europe/Amsterdam")
    >>> d.to_instant()
    Instant("2020-08-15 21:00:00Z")
    
to_plain() → PlainDateTime¶
    
Get the underlying date and time (without offset or timezone)
As an inverse, `PlainDateTime` has methods `assume_utc()`, `assume_fixed_offset()` , `assume_tz()`, and `assume_system_tz()` which may require additional arguments.
#### Concrete classes¶
class whenever.Instant¶
    
Bases: `_ExactTime`
Represents a moment in time with nanosecond precision.
This class is great for representing a specific point in time independent of location. It maps 1:1 to UTC or a UNIX timestamp.
Example
    
    >>> from whenever import Instant
    >>> py311_release = Instant.from_utc(2022, 10, 24, hour=17)
    Instant("2022-10-24 17:00:00Z")
    >>> py311_release.add(hours=3).timestamp()
    1666641600
    
MIN: ClassVar[Instant] = Instant("0001-01-01 00:00:00Z")¶
    
MAX: ClassVar[Instant] = Instant("9999-12-31 23:59:59.999999999Z")¶
    
classmethod from_utc(year: int, month: int, day: int, hour: int = 0, minute: int = 0, second: int = 0, *, nanosecond: int = 0) → Instant¶
    
Create an Instant defined by a UTC date and time.
format_iso(*, unit: Literal['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond', 'auto'] = 'auto', basic: bool = False, sep: Literal['T', ' '] = 'T') → str¶
    
Convert to the ISO 8601 string representation.
The inverse of the `parse_iso()` method.
format_rfc2822() → str¶
    
Format as an RFC 2822 string.
The inverse of the `parse_rfc2822()` method.
Note
The output is also compatible with the (stricter) RFC 9110 standard.
Example
    
    >>> Instant.from_utc(2020, 8, 8, hour=23, minute=12).format_rfc2822()
    "Sat, 08 Aug 2020 23:12:00 GMT"
    
classmethod parse_rfc2822(s: str, /) → Instant¶
    
Parse a UTC datetime in RFC 2822 format.
The inverse of the `format_rfc2822()` method.
Example
    
    >>> Instant.parse_rfc2822("Sat, 15 Aug 2020 23:12:00 GMT")
    Instant("2020-08-15 23:12:00Z")
    
    
    >>> # also valid:
    >>> Instant.parse_rfc2822("Sat, 15 Aug 2020 23:12:00 +0000")
    >>> Instant.parse_rfc2822("Sat, 15 Aug 2020 23:12:00 +0800")
    >>> Instant.parse_rfc2822("Sat, 15 Aug 2020 23:12:00 -0000")
    >>> Instant.parse_rfc2822("Sat, 15 Aug 2020 23:12:00 UT")
    >>> Instant.parse_rfc2822("Sat, 15 Aug 2020 23:12:00 MST")
    
Note
  * Although technically part of the RFC 2822 standard, comments within folding whitespace are not supported.


add(*, hours: float = 0, minutes: float = 0, seconds: float = 0, milliseconds: float = 0, microseconds: float = 0, nanoseconds: int = 0) → Instant¶
    
Add a time amount to this instant.
See the docs on arithmetic for more information.
subtract(*, hours: float = 0, minutes: float = 0, seconds: float = 0, milliseconds: float = 0, microseconds: float = 0, nanoseconds: int = 0) → Instant¶
    
Subtract a time amount from this instant.
See the docs on arithmetic for more information.
round(unit: Literal['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'] = 'second', increment: int = 1, mode: Literal['ceil', 'floor', 'half_ceil', 'half_floor', 'half_even'] = 'half_even') → Instant¶
    
Round the instant to the specified unit and increment. Various rounding modes are available.
Examples
    
    >>> Instant.from_utc(2020, 1, 1, 12, 39, 59).round("minute", 15)
    Instant("2020-01-01 12:45:00Z")
    >>> Instant.from_utc(2020, 1, 1, 8, 9, 13).round("second", 5, mode="floor")
    Instant("2020-01-01 08:09:10Z")
    
__add__(delta: TimeDelta) → Instant¶
    
Add a time amount to this datetime.
See the docs on arithmetic for more information.
__sub__(other: TimeDelta | _ExactTime) → Instant | TimeDelta¶
    
Subtract another exact time or timedelta
Subtraction of deltas happens in the same way as the `subtract()` method. Subtraction of instants happens the same way as the `difference()` method.
See the docs on arithmetic for more information.
Example
    
    >>> d = Instant.from_utc(2020, 8, 15, hour=23, minute=12)
    >>> d - hours(24) - seconds(5)
    Instant("2020-08-14 23:11:55Z")
    >>> d - Instant.from_utc(2020, 8, 14)
    TimeDelta(47:12:00)
    
class whenever.PlainDateTime(year: int, month: int, day: int, hour: int = 0, minute: int = 0, second: int = 0, *, nanosecond: int = 0)¶
    
Bases: `_LocalTime`
A combination of date and time-of-day, without a timezone.
Can be used to represent local time, i.e. how time appears to people on a wall clock.
It can’t be mixed with exact time types (e.g. `Instant`, `ZonedDateTime`) Conversion to exact time types can only be done by explicitly assuming a timezone or offset.
Examples of when to use this type:
  * You need to express a date and time as it would be observed locally on the “wall clock” or calendar.
  * You receive a date and time without any timezone information, and you need a type to represent this lack of information.
  * In the rare case you truly don’t need to account for timezones, or Daylight Saving Time transitions. For example, when modeling time in a simulation game.


MIN: ClassVar[PlainDateTime] = PlainDateTime("0001-01-01 00:00:00")¶
    
MAX: ClassVar[PlainDateTime] = PlainDateTime("9999-12-31 23:59:59.999999999")¶
    
format_iso(*, unit: Literal['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond', 'auto'] = 'auto', basic: bool = False, sep: Literal['T', ' '] = 'T') → str¶
    
Convert to the popular ISO format `YYYY-MM-DDTHH:MM:SS`
The inverse of the `parse_iso()` method.
assume_utc() → Instant¶
    
Assume the datetime is in UTC, creating an `Instant`.
Example
    
    >>> PlainDateTime(2020, 8, 15, 23, 12).assume_utc()
    Instant("2020-08-15 23:12:00Z")
    
assume_fixed_offset(offset: int | TimeDelta, /) → OffsetDateTime¶
    
Assume the datetime has the given offset, creating an `OffsetDateTime`.
Example
    
    >>> PlainDateTime(2020, 8, 15, 23, 12).assume_fixed_offset(+2)
    OffsetDateTime("2020-08-15 23:12:00+02:00")
    
assume_tz(tz: str, /, disambiguate: Literal['compatible', 'earlier', 'later', 'raise'] = 'compatible') → ZonedDateTime¶
    
Assume the datetime is in the given timezone, creating a `ZonedDateTime`.
Note
The local time may be ambiguous in the given timezone (e.g. during a DST transition). You can explicitly specify how to handle such a situation using the `disambiguate` argument. See the documentation for more information.
Example
    
    >>> d = PlainDateTime(2020, 8, 15, 23, 12)
    >>> d.assume_tz("Europe/Amsterdam", disambiguate="raise")
    ZonedDateTime("2020-08-15 23:12:00+02:00[Europe/Amsterdam]")
    
assume_system_tz(disambiguate: Literal['compatible', 'earlier', 'later', 'raise'] = 'compatible') → ZonedDateTime¶
    
Assume the datetime is in the system timezone, creating a `ZonedDateTime`.
Note
The local time may be ambiguous in the system timezone (e.g. during a DST transition). You can explicitly specify how to handle such a situation using the `disambiguate` argument. See the documentation for more information.
Example
    
    >>> d = PlainDateTime(2020, 8, 15, 23, 12)
    >>> # assuming system timezone is America/New_York
    >>> d.assume_system_tz(disambiguate="raise")
    ZonedDateTime("2020-08-15 23:12:00-04:00[America/New_York]")
    
classmethod parse_strptime(s: str, /, *, format: str) → PlainDateTime¶
    
Parse a plain datetime using the standard library `strptime()` method.
Example
    
    >>> PlainDateTime.parse_strptime("2020-08-15", format="%d/%m/%Y_%H:%M")
    PlainDateTime("2020-08-15 00:00:00")
    
Note
This method defers to the standard library `strptime()` method, which may behave differently in different Python versions. It also only supports up to microsecond precision.
Important
There may not be an offset in the format string. This means you CANNOT use the directives `%z`, `%Z`, or `%:z`. Use `OffsetDateTime` to parse datetimes with an offset.
difference(other: PlainDateTime, /, *, ignore_dst: bool = False) → TimeDelta¶
    
Calculate the difference between two times without a timezone.
Important
The difference between two datetimes without a timezone implicitly ignores DST transitions and other timezone changes. To perform DST-safe operations, convert to a `ZonedDateTime` first. Or, if you don’t know the timezone and accept potentially incorrect results during DST transitions, pass `ignore_dst=True`. For more information, see the docs.
__eq__(other: object) → bool¶
    
Compare objects for equality. Only ever equal to other `PlainDateTime` instances with the same values.
Warning
To comply with the Python data model, this method can’t raise a `TypeError` when comparing with other types. Although it seems to be the sensible response, it would result in surprising behavior when using values as dictionary keys.
Use mypy’s `--strict-equality` flag to detect and prevent this.
Example
    
    >>> PlainDateTime(2020, 8, 15, 23) == PlainDateTime(2020, 8, 15, 23)
    True
    >>> PlainDateTime(2020, 8, 15, 23, 1) == PlainDateTime(2020, 8, 15, 23)
    False
    >>> PlainDateTime(2020, 8, 15) == Instant.from_utc(2020, 8, 15)
    False  # Use mypy's --strict-equality flag to detect this.
    
class whenever.OffsetDateTime(year: int, month: int, day: int, hour: int = 0, minute: int = 0, second: int = 0, *, nanosecond: int = 0, offset: int | TimeDelta)¶
    
Bases: `_ExactAndLocalTime`
A datetime with a fixed UTC offset. Useful for representing a “static” local date and time-of-day at a specific location.
Example
    
    >>> # Midnight in Salt Lake City
    >>> OffsetDateTime(2023, 4, 21, offset=-6)
    OffsetDateTime("2023-04-21 00:00:00-06:00")
    
Note
Adjusting instances of this class do not account for daylight saving time. If you need to add or subtract durations from an offset datetime and account for DST, convert to a `ZonedDateTime` first, This class knows when the offset changes.
format_iso(*, unit: Literal['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond', 'auto'] = 'auto', basic: bool = False, sep: Literal['T', ' '] = 'T') → str¶
    
Convert to the popular ISO format `YYYY-MM-DDTHH:MM:SS±HH:MM`
The inverse of the `parse_iso()` method.
format_rfc2822() → str¶
    
Format as an RFC 2822 string.
The inverse of the `parse_rfc2822()` method.
Example
    
    >>> OffsetDateTime(2020, 8, 15, 23, 12, offset=hours(2)).format_rfc2822()
    "Sat, 15 Aug 2020 23:12:00 +0200"
    
classmethod parse_rfc2822(s: str, /) → OffsetDateTime¶
    
Parse an offset datetime in RFC 2822 format.
The inverse of the `format_rfc2822()` method.
Example
    
    >>> OffsetDateTime.parse_rfc2822("Sat, 15 Aug 2020 23:12:00 +0200")
    OffsetDateTime("2020-08-15 23:12:00+02:00")
    >>> # also valid:
    >>> OffsetDateTime.parse_rfc2822("Sat, 15 Aug 2020 23:12:00 UT")
    >>> OffsetDateTime.parse_rfc2822("Sat, 15 Aug 2020 23:12:00 GMT")
    >>> OffsetDateTime.parse_rfc2822("Sat, 15 Aug 2020 23:12:00 MST")
    
Note
  * Strictly speaking, an offset of `-0000` means that the offset is “unknown”. Here, we treat it the same as +0000.
  * Although technically part of the RFC 2822 standard, comments within folding whitespace are not supported.


classmethod parse_strptime(s: str, /, *, format: str) → OffsetDateTime¶
    
Parse a datetime with offset using the standard library `strptime()` method.
Example
    
    >>> OffsetDateTime.parse_strptime("2020-08-15+0200", format="%Y-%m-%d%z")
    OffsetDateTime("2020-08-15 00:00:00+02:00")
    
Note
This method defers to the standard library `strptime()` method, which may behave differently in different Python versions. It also only supports up to microsecond precision.
Important
An offset must be present in the format string. This means you MUST include the directive `%z`, `%Z`, or `%:z`. To parse a datetime without an offset, use `PlainDateTime` instead.
class whenever.ZonedDateTime(year: int, month: int, day: int, hour: int = 0, minute: int = 0, second: int = 0, *, nanosecond: int = 0, tz: str, disambiguate: Literal['compatible', 'earlier', 'later', 'raise'] = 'compatible')¶
    
Bases: `_ExactAndLocalTime`
A datetime associated with a timezone in the IANA database. Useful for representing the exact time at a specific location.
Example
    
    >>> ZonedDateTime(2024, 12, 8, hour=11, tz="Europe/Paris")
    ZonedDateTime("2024-12-08 11:00:00+01:00[Europe/Paris]")
    >>> # Explicitly resolve ambiguities during DST transitions
    >>> ZonedDateTime(2023, 10, 29, 1, 15, tz="Europe/London", disambiguate="earlier")
    ZonedDateTime("2023-10-29 01:15:00+01:00[Europe/London]")
    
Important
To use this type properly, read more about ambiguity in timezones.
format_iso(*, unit: Literal['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond', 'auto'] = 'auto', basic: bool = False, sep: Literal['T', ' '] = 'T', tz: Literal['always', 'never', 'auto'] = 'always') → str¶
    
Convert to the popular ISO format `YYYY-MM-DDTHH:MM:SS±HH:MM[TZ_ID]`.
The inverse of the `parse_iso()` method.
Use the `unit` parameter to control the precision of the time part, the `sep` parameter to control the separator, and the `basic` parameter to use the basic ISO format instead of the extended one.
Example
    
    >>> ZonedDateTime(2020, 8, 15, hour=23, minute=12, tz="Europe/London")
    ZonedDateTime("2020-08-15 23:12:00+01:00[Europe/London]")
    
Important
The timezone ID is a recent extension to the ISO 8601 format (RFC 9557). Althought it is gaining popularity, it is not yet widely supported by ISO 8601 parsers.
classmethod now_in_system_tz() → ZonedDateTime¶
    
Create an instance from the current time in the system timezone.
Equivalent to `Instant.now().to_system_tz()`.
classmethod from_system_tz(year: int, month: int, day: int, hour: int = 0, minute: int = 0, second: int = 0, *, nanosecond: int = 0, disambiguate: Literal['compatible', 'earlier', 'later', 'raise'] = 'compatible') → ZonedDateTime¶
    
Create an instance in the system timezone.
Equivalent to `ZonedDateTime(..., tz=<the system timezone>)`, except it also works for system timezones whose corresponding IANA timezone ID is unknown.
Example
    
    >>> ZonedDateTime.from_system_tz(2020, 8, 15, hour=23, minute=12)
    ZonedDateTime("2020-08-15 23:12:00+02:00[Europe/Berlin]")
    
property tz: str | None¶
    
The timezone ID. In rare cases, this may be `None`, if the `ZonedDateTime` was created from a system timezone without a known IANA key.
is_ambiguous() → bool¶
    
Whether the date and time-of-day are ambiguous, e.g. due to a DST transition.
Example
    
    >>> ZonedDateTime(2020, 8, 15, 23, tz="Europe/London").is_ambiguous()
    False
    >>> ZonedDateTime(2023, 10, 29, 2, 15, tz="Europe/Amsterdam").is_ambiguous()
    True
    
start_of_day() → ZonedDateTime¶
    
The start of the current calendar day.
This is almost always at midnight the same day, but may be different for timezones which transition at—and thus skip over—midnight.
day_length() → TimeDelta¶
    
The duration between the start of the current day and the next. This is usually 24 hours, but may be different due to timezone transitions.
Example
    
    >>> ZonedDateTime(2020, 8, 15, tz="Europe/London").day_length()
    TimeDelta(24:00:00)
    >>> ZonedDateTime(2023, 10, 29, tz="Europe/Amsterdam").day_length()
    TimeDelta(25:00:00)
    
### Deltas¶
whenever.years(i: int, /) → DateDelta¶
    
Create a `DateDelta` with the given number of years. `years(1) == DateDelta(years=1)`
whenever.months(i: int, /) → DateDelta¶
    
Create a `DateDelta` with the given number of months. `months(1) == DateDelta(months=1)`
whenever.weeks(i: int, /) → DateDelta¶
    
Create a `DateDelta` with the given number of weeks. `weeks(1) == DateDelta(weeks=1)`
whenever.days(i: int, /) → DateDelta¶
    
Create a `DateDelta` with the given number of days. `days(1) == DateDelta(days=1)`
whenever.hours(i: float, /) → TimeDelta¶
    
Create a `TimeDelta` with the given number of hours. `hours(1) == TimeDelta(hours=1)`
whenever.minutes(i: float, /) → TimeDelta¶
    
Create a `TimeDelta` with the given number of minutes. `minutes(1) == TimeDelta(minutes=1)`
whenever.seconds(i: float, /) → TimeDelta¶
    
Create a `TimeDelta` with the given number of seconds. `seconds(1) == TimeDelta(seconds=1)`
whenever.milliseconds(i: int, /) → TimeDelta¶
    
Create a `TimeDelta` with the given number of milliseconds. `milliseconds(1) == TimeDelta(milliseconds=1)`
whenever.microseconds(i: float, /) → TimeDelta¶
    
Create a `TimeDelta` with the given number of microseconds. `microseconds(1) == TimeDelta(microseconds=1)`
whenever.nanoseconds(i: int, /) → TimeDelta¶
    
Create a `TimeDelta` with the given number of nanoseconds. `nanoseconds(1) == TimeDelta(nanoseconds=1)`
class whenever.TimeDelta(*, hours: float = 0, minutes: float = 0, seconds: float = 0, milliseconds: float = 0, microseconds: float = 0, nanoseconds: int = 0)¶
    
A duration consisting of a precise time: hours, minutes, (nano)seconds
The inputs are normalized, so 90 minutes becomes 1 hour and 30 minutes, for example.
Examples
    
    >>> d = TimeDelta(hours=1, minutes=30)
    TimeDelta("PT1h30m")
    >>> d.in_minutes()
    90.0
    
Note
A shorter way to instantiate a timedelta is to use the helper functions `hours()`, `minutes()`, etc.
MIN: ClassVar[TimeDelta] = TimeDelta("-PT87831216h")¶
    
MAX: ClassVar[TimeDelta] = TimeDelta("PT87831216h")¶
    
__abs__() → TimeDelta¶
    
The absolute value
Example
    
    >>> d = TimeDelta(hours=-1, minutes=-30)
    >>> abs(d)
    TimeDelta("PT1h30m")
    
__add__(other: TimeDelta) → TimeDelta¶
    
Add two deltas together
Example
    
    >>> d = TimeDelta(hours=1, minutes=30)
    >>> d + TimeDelta(minutes=30)
    TimeDelta("PT2h")
    
__bool__() → bool¶
    
True if the value is non-zero
Example
    
    >>> bool(TimeDelta())
    False
    >>> bool(TimeDelta(minutes=1))
    True
    
__eq__(other: object) → bool¶
    
Compare for equality
Example
    
    >>> d = TimeDelta(hours=1, minutes=30)
    >>> d == TimeDelta(minutes=90)
    True
    >>> d == TimeDelta(hours=2)
    False
    
__gt__(other: TimeDelta) → bool¶
    
Return self>value.
__mul__(other: float) → TimeDelta¶
    
Multiply by a number
Example
    
    >>> d = TimeDelta(hours=1, minutes=30)
    >>> d * 2.5
    TimeDelta("PT3h45m")
    
__neg__() → TimeDelta¶
    
Negate the value
Example
    
    >>> d = TimeDelta(hours=1, minutes=30)
    >>> -d
    TimeDelta(-PT1h30m)
    
__sub__(other: TimeDelta) → TimeDelta¶
    
Subtract two deltas
Example
    
    >>> d = TimeDelta(hours=1, minutes=30)
    >>> d - TimeDelta(minutes=30)
    TimeDelta("PT1h")
    
__truediv__(other: float | TimeDelta) → TimeDelta | float¶
    
Divide by a number or another delta
Example
    
    >>> d = TimeDelta(hours=1, minutes=30)
    >>> d / 2.5
    TimeDelta("PT36m")
    >>> d / TimeDelta(minutes=30)
    3.0
    
Note
Because TimeDelta is limited to nanosecond precision, the result of division may not be exact.
format_iso() → str¶
    
Format as the popular interpretation of the ISO 8601 duration format. May not strictly adhere to (all versions of) the standard. See here for more information.
Inverse of `parse_iso()`.
Example
    
    >>> TimeDelta(hours=1, minutes=30).format_iso()
    'PT1H30M'
    
classmethod from_py_timedelta(td: timedelta, /) → TimeDelta¶
    
Create from a `timedelta`
Inverse of `py_timedelta()`
Example
    
    >>> TimeDelta.from_py_timedelta(timedelta(seconds=5400))
    TimeDelta("PT1h30m")
    
Note
Subclasses of `timedelta` are not accepted because they often add additional state that cannot be represented in a `TimeDelta`.
in_days_of_24h() → float¶
    
The total size in days (of exactly 24 hours each)
Note
Note that this may not be the same as days on the calendar, since some days have 23 or 25 hours due to daylight saving time.
in_hours() → float¶
    
The total size in hours
Example
    
    >>> d = TimeDelta(hours=1, minutes=30)
    >>> d.in_hours()
    1.5
    
in_hrs_mins_secs_nanos() → tuple[int, int, int, int]¶
    
Convert to a tuple of (hours, minutes, seconds, nanoseconds)
Example
    
    >>> d = TimeDelta(hours=1, minutes=30, microseconds=5_000_090)
    >>> d.in_hrs_mins_secs_nanos()
    (1, 30, 5, 90_000)
    
in_microseconds() → float¶
    
The total size in microseconds
    
    >>> d = TimeDelta(seconds=2, nanoseconds=50)
    >>> d.in_microseconds()
    2_000_000.05
    
in_milliseconds() → float¶
    
The total size in milliseconds
    
    >>> d = TimeDelta(seconds=2, microseconds=50)
    >>> d.in_milliseconds()
    2_000.05
    
in_minutes() → float¶
    
The total size in minutes
Example
    
    >>> d = TimeDelta(hours=1, minutes=30, seconds=30)
    >>> d.in_minutes()
    90.5
    
in_nanoseconds() → int¶
    
The total size in nanoseconds
    
    >>> d = TimeDelta(seconds=2, nanoseconds=50)
    >>> d.in_nanoseconds()
    2_000_000_050
    
in_seconds() → float¶
    
The total size in seconds
Example
    
    >>> d = TimeDelta(minutes=2, seconds=1, microseconds=500_000)
    >>> d.in_seconds()
    121.5
    
classmethod parse_iso(s: str, /) → TimeDelta¶
    
Parse the popular interpretation of the ISO 8601 duration format. Does not parse all possible ISO 8601 durations. See here for more information.
Inverse of `format_iso()`
Example
    
    >>> TimeDelta.parse_iso("PT1H80M")
    TimeDelta("PT2h20m")
    
Note
Any duration with a date part is considered invalid. `PT0S` is valid, but `P0D` is not.
py_timedelta() → timedelta¶
    
Convert to a `timedelta`
Inverse of `from_py_timedelta()`
Note
Nanoseconds are truncated to microseconds. If you need more control over rounding, use `round()` first.
Example
    
    >>> d = TimeDelta(hours=1, minutes=30)
    >>> d.py_timedelta()
    timedelta(seconds=5400)
    
round(unit: Literal['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'] = 'second', increment: int = 1, mode: Literal['ceil', 'floor', 'half_ceil', 'half_floor', 'half_even'] = 'half_even') → TimeDelta¶
    
Round the delta to the specified unit and increment. Various rounding modes are available.
Examples
    
    >>> t = TimeDelta(seconds=12345)
    TimeDelta("PT3h25m45s")
    >>> t.round("minute")
    TimeDelta("PT3h26m")
    >>> t.round("second", increment=10, mode="floor")
    TimeDelta("PT3h25m40s")
    
class whenever.DateDelta(*, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0)¶
    
A duration of time consisting of calendar units (years, months, weeks, and days)
__abs__() → DateDelta¶
    
If the contents are negative, return the positive version
Example
    
    >>> p = DateDelta(months=-2, days=-3)
    >>> abs(p)
    DateDelta("P2m3d")
    
__add__(other: DateDelta | TimeDelta) → DateDelta | DateTimeDelta¶
    
Add the fields of another delta to this one
Example
    
    >>> p = DateDelta(weeks=2, months=1)
    >>> p + DateDelta(weeks=1, days=4)
    DateDelta("P1m25d")
    
__bool__() → bool¶
    
True if any contains any non-zero data
Example
    
    >>> bool(DateDelta())
    False
    >>> bool(DateDelta(days=-1))
    True
    
__eq__(other: object) → bool¶
    
Compare for equality, normalized to months and days.
a == b is equivalent to a.in_months_days() == b.in_months_days()
Example
    
    >>> p = DateDelta(weeks=4, days=2)
    DateDelta("P30d")
    >>> p == DateDelta(weeks=3, days=9)
    True
    >>> p == DateDelta(weeks=2, days=4)
    True  # same number of days
    >>> p == DateDelta(months=1)
    False  # months and days cannot be compared directly
    
__mul__(other: int) → DateDelta¶
    
Multiply the contents by a round number
Example
    
    >>> p = DateDelta(years=1, weeks=2)
    >>> p * 2
    DateDelta("P2y28d")
    
__neg__() → DateDelta¶
    
Negate the contents
Example
    
    >>> p = DateDelta(weeks=2, days=3)
    >>> -p
    DateDelta(-P17d)
    
__sub__(other: DateDelta | TimeDelta) → DateDelta | DateTimeDelta¶
    
Subtract the fields of another delta from this one
Example
    
    >>> p = DateDelta(weeks=2, days=3)
    >>> p - DateDelta(days=2)
    DateDelta("P15d")
    
format_iso() → str¶
    
Format as the popular interpretation of the ISO 8601 duration format. May not strictly adhere to (all versions of) the standard. See here for more information.
Inverse of `parse_iso()`.
The format looks like this:
    
    P(nY)(nM)(nD)
    
For example:
    
    P1D
    P2M
    P1Y2M3W4D
    
Example
    
    >>> p = DateDelta(years=1, months=2, weeks=3, days=11)
    >>> p.format_iso()
    'P1Y2M3W11D'
    >>> DateDelta().format_iso()
    'P0D'
    
in_months_days() → tuple[int, int]¶
    
Convert to a tuple of months and days.
Example
    
    >>> p = DateDelta(months=25, days=9)
    >>> p.in_months_days()
    (25, 9)
    >>> DateDelta(months=-13, weeks=-5)
    (-13, -35)
    
in_years_months_days() → tuple[int, int, int]¶
    
Convert to a tuple of years, months, and days.
Example
    
    >>> p = DateDelta(years=1, months=2, days=11)
    >>> p.in_years_months_days()
    (1, 2, 11)
    
classmethod parse_iso(s: str, /) → DateDelta¶
    
Parse the popular interpretation of the ISO 8601 duration format. Does not parse all possible ISO 8601 durations. See here for more information.
Inverse of `format_iso()`
Example
    
    >>> DateDelta.parse_iso("P1W11D")
    DateDelta("P1w11d")
    >>> DateDelta.parse_iso("-P3m")
    DateDelta(-P3m)
    
Note
Only durations without time component are accepted. `P0D` is valid, but `PT0S` is not.
Note
The number of digits in each component is limited to 8.
class whenever.DateTimeDelta(*, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0, hours: float = 0, minutes: float = 0, seconds: float = 0, milliseconds: float = 0, microseconds: float = 0, nanoseconds: int = 0)¶
    
A duration with both a date and time component.
__abs__() → DateTimeDelta¶
    
The absolute value of the delta
Example
    
    >>> d = DateTimeDelta(weeks=1, days=-11, hours=4)
    >>> abs(d)
    DateTimeDelta("P1w11dT4h")
    
__add__(other: DateTimeDelta | TimeDelta | DateDelta) → DateTimeDelta¶
    
Add two deltas together
Example
    
    >>> d = DateTimeDelta(weeks=1, days=11, hours=4)
    >>> d + DateTimeDelta(months=2, days=3, minutes=90)
    DateTimeDelta("P1m1w14dT5h30m")
    
__bool__() → bool¶
    
True if any field is non-zero
Example
    
    >>> bool(DateTimeDelta())
    False
    >>> bool(DateTimeDelta(minutes=1))
    True
    
__eq__(other: object) → bool¶
    
Compare for equality
Example
    
    >>> d = DateTimeDelta(
    ...     weeks=1,
    ...     days=23,
    ...     hours=4,
    ... )
    >>> d == DateTimeDelta(
    ...     weeks=1,
    ...     days=23,
    ...     minutes=4 * 60,  # normalized
    ... )
    True
    >>> d == DateTimeDelta(
    ...     weeks=4,
    ...     days=2,  # days/weeks are normalized
    ...     hours=4,
    ... )
    True
    >>> d == DateTimeDelta(
    ...     months=1,  # months/days cannot be compared directly
    ...     hours=4,
    ... )
    False
    
__mul__(other: int) → DateTimeDelta¶
    
Multiply by a number
Example
    
    >>> d = DateTimeDelta(weeks=1, days=11, hours=4)
    >>> d * 2
    DateTimeDelta("P2w22dT8h")
    
__neg__() → DateTimeDelta¶
    
Negate the delta
Example
    
    >>> d = DateTimeDelta(days=11, hours=4)
    >>> -d
    DateTimeDelta(-P11dT4h)
    
__sub__(other: DateTimeDelta | TimeDelta | DateDelta) → DateTimeDelta¶
    
Subtract two deltas
Example
    
    >>> d = DateTimeDelta(weeks=1, days=11, hours=4)
    >>> d - DateTimeDelta(months=2, days=3, minutes=90)
    DateTimeDelta(-P2m1w8dT2h30m)
    
date_part() → DateDelta¶
    
The date part of the delta
format_iso() → str¶
    
Format as the popular interpretation of the ISO 8601 duration format. May not strictly adhere to (all versions of) the standard. See here for more information.
Inverse of `parse_iso()`.
The format is:
    
    P(nY)(nM)(nD)T(nH)(nM)(nS)
    
Example
    
    >>> d = DateTimeDelta(
    ...     weeks=1,
    ...     days=11,
    ...     hours=4,
    ...     milliseconds=12,
    ... )
    >>> d.format_iso()
    'P1W11DT4H0.012S'
    
in_months_days_secs_nanos() → tuple[int, int, int, int]¶
    
Convert to a tuple of (months, days, seconds, nanoseconds)
Example
    
    >>> d = DateTimeDelta(weeks=1, days=11, hours=4, microseconds=2)
    >>> d.in_months_days_secs_nanos()
    (0, 18, 14_400, 2000)
    
classmethod parse_iso(s: str, /) → DateTimeDelta¶
    
Parse the popular interpretation of the ISO 8601 duration format. Does not parse all possible ISO 8601 durations. See here for more information.
Examples:
    
    P4D        # 4 days
    PT4H       # 4 hours
    PT3M40.5S  # 3 minutes and 40.5 seconds
    P1W11DT4H  # 1 week, 11 days, and 4 hours
    -PT7H4M    # -7 hours and -4 minutes (-7:04:00)
    +PT7H4M    # 7 hours and 4 minutes (7:04:00)
    
Inverse of `format_iso()`
Example
    
    >>> DateTimeDelta.parse_iso("-P1W11DT4H")
    DateTimeDelta(-P1w11dT4h)
    
time_part() → TimeDelta¶
    
The time part of the delta
### Date and time components¶
class whenever.Date(year: int, month: int, day: int)¶
    
A date without a time component.
    
    >>> d = Date(2021, 1, 2)
    Date("2021-01-02")
    
Can also be constructed directly from an ISO 8601 string.
    
    >>> Date("2021-01-02")
    Date("2021-01-02")
    
MIN: ClassVar[Date] = Date("0001-01-01")¶
    
MAX: ClassVar[Date] = Date("9999-12-31")¶
    
__add__(p: DateDelta) → Date¶
    
Add a delta to a date. Behaves the same as `add()`
__eq__(other: object) → bool¶
    
Compare for equality
Example
    
    >>> d = Date(2021, 1, 2)
    >>> d == Date(2021, 1, 2)
    True
    >>> d == Date(2021, 1, 3)
    False
    
__ge__(other: Date) → bool¶
    
Return self>=value.
__gt__(other: Date) → bool¶
    
Return self>value.
__le__(other: Date) → bool¶
    
Return self<=value.
__lt__(other: Date) → bool¶
    
Return self<value.
__sub__(d: DateDelta | Date) → Date | DateDelta¶
    
Subtract a delta from a date, or subtract two dates
Subtracting a delta works the same as `subtract()`.
    
    >>> Date(2021, 1, 2) - DateDelta(weeks=1, days=3)
    Date("2020-12-26")
    
The difference between two dates is calculated in months and days, such that:
    
    >>> delta = d1 - d2
    >>> d2 + delta == d1  # always
    
The following is not always true:
    
    >>> d1 - (d1 - d2) == d2  # not always true!
    >>> -(d2 - d1) == d1 - d2  # not always true!
    
Examples:
    
    >>> Date(2023, 4, 15) - Date(2011, 6, 24)
    DateDelta("P12Y9M22D")
    >>> # Truncation
    >>> Date(2024, 4, 30) - Date(2023, 5, 31)
    DateDelta("P11M")
    >>> Date(2024, 3, 31) - Date(2023, 6, 30)
    DateDelta("P9M1D")
    >>> # the other way around, the result is different
    >>> Date(2023, 6, 30) - Date(2024, 3, 31)
    DateDelta(-P9M)
    
Note
If you’d like to calculate the difference in days only (no months), use the `days_until()` or `days_since()` instead.
add(*args, **kwargs) → Date¶
    
Add a components to a date.
See the docs on arithmetic for more information.
Example
    
    >>> d = Date(2021, 1, 2)
    >>> d.add(years=1, months=2, days=3)
    Date("2022-03-05")
    >>> Date(2020, 2, 29).add(years=1)
    Date("2021-02-28")
    
at(t: Time, /) → PlainDateTime¶
    
Combine a date with a time to create a datetime
Example
    
    >>> d = Date(2021, 1, 2)
    >>> d.at(Time(12, 30))
    PlainDateTime("2021-01-02 12:30:00")
    
You can use methods like `assume_utc()` or `assume_tz()` to find the corresponding exact time.
day_of_week() → Weekday¶
    
The day of the week
Example
    
    >>> Date(2021, 1, 2).day_of_week()
    Weekday.SATURDAY
    >>> Weekday.SATURDAY.value
    6  # the ISO value
    
days_since(other: Date, /) → int¶
    
Calculate the number of days this day is after another date. If the other date is after this date, the result is negative.
Example
    
    >>> Date(2021, 1, 5).days_since(Date(2021, 1, 2))
    3
    
Note
If you’re interested in calculating the difference in terms of days and months, use the subtraction operator instead.
days_until(other: Date, /) → int¶
    
Calculate the number of days from this date to another date. If the other date is before this date, the result is negative.
Example
    
    >>> Date(2021, 1, 2).days_until(Date(2021, 1, 5))
    3
    
Note
If you’re interested in calculating the difference in terms of days and months, use the subtraction operator instead.
format_iso(*, basic: bool = False) → str¶
    
Format as the ISO 8601 date format.
Inverse of `parse_iso()`.
Example
    
    >>> Date(2021, 1, 2).format_iso()
    '2021-01-02'
    >>> Date(1992, 9, 4).format_iso(basic=True)
    '19920904'
    
classmethod from_py_date(d: date, /) → Date¶
    
Create from a `date`
Example
    
    >>> Date.from_py_date(date(2021, 1, 2))
    Date("2021-01-02")
    
month_day() → MonthDay¶
    
The month and day (without a year component)
Example
    
    >>> Date(2021, 1, 2).month_day()
    MonthDay("--01-02")
    
classmethod parse_iso(s: str, /) → Date¶
    
Parse a date from an ISO8601 string
The following formats are accepted: \- `YYYY-MM-DD` (“extended” format) \- `YYYYMMDD` (“basic” format)
Inverse of `format_iso()`
Example
    
    >>> Date.parse_iso("2021-01-02")
    Date("2021-01-02")
    
py_date() → date¶
    
Convert to a standard library `date`
replace(**kwargs: Any) → Date¶
    
Create a new instance with the given fields replaced
Example
    
    >>> d = Date(2021, 1, 2)
    >>> d.replace(day=4)
    Date("2021-01-04")
    
subtract(*args, **kwargs) → Date¶
    
Subtract components from a date.
See the docs on arithmetic for more information.
Example
    
    >>> d = Date(2021, 1, 2)
    >>> d.subtract(years=1, months=2, days=3)
    Date("2019-10-30")
    >>> Date(2021, 3, 1).subtract(years=1)
    Date("2020-03-01")
    
classmethod today_in_system_tz() → Date¶
    
Get the current date in the system’s local timezone.
Alias for `Instant.now().to_system_tz().date()`.
Example
    
    >>> Date.today_in_system_tz()
    Date("2021-01-02")
    
year_month() → YearMonth¶
    
The year and month (without a day component)
Example
    
    >>> Date(2021, 1, 2).year_month()
    YearMonth("2021-01")
    
class whenever.YearMonth(year: int, month: int)¶
    
A year and month without a day component
Useful for representing recurring events or billing periods.
Example
    
    >>> ym = YearMonth(2021, 1)
    YearMonth("2021-01")
    
MIN: ClassVar[YearMonth] = YearMonth("0001-01")¶
    
MAX: ClassVar[YearMonth] = YearMonth("9999-12")¶
    
__eq__(other: object) → bool¶
    
Compare for equality
Example
    
    >>> ym = YearMonth(2021, 1)
    >>> ym == YearMonth(2021, 1)
    True
    >>> ym == YearMonth(2021, 2)
    False
    
__ge__(other: YearMonth) → bool¶
    
Return self>=value.
__gt__(other: YearMonth) → bool¶
    
Return self>value.
__le__(other: YearMonth) → bool¶
    
Return self<=value.
__lt__(other: YearMonth) → bool¶
    
Return self<value.
format_iso() → str¶
    
Format as the ISO 8601 year-month format.
Inverse of `parse_iso()`.
Example
    
    >>> YearMonth(2021, 1).format_iso()
    '2021-01'
    
on_day(day: int, /) → Date¶
    
Create a date from this year-month with a given day
Example
    
    >>> YearMonth(2021, 1).on_day(2)
    Date("2021-01-02")
    
classmethod parse_iso(s: str, /) → YearMonth¶
    
Create from the ISO 8601 format `YYYY-MM` or `YYYYMM`.
Inverse of `format_iso()`
Example
    
    >>> YearMonth.parse_iso("2021-01")
    YearMonth("2021-01")
    
replace(**kwargs: Any) → YearMonth¶
    
Create a new instance with the given fields replaced
Example
    
    >>> d = YearMonth(2021, 12)
    >>> d.replace(month=3)
    YearMonth("2021-03")
    
class whenever.MonthDay(month: int, day: int)¶
    
A month and day without a year component.
Useful for representing recurring events or birthdays.
Example
    
    >>> MonthDay(11, 23)
    MonthDay("--11-23")
    
MIN: ClassVar[MonthDay] = MonthDay("--01-01")¶
    
MAX: ClassVar[MonthDay] = MonthDay("--12-31")¶
    
__eq__(other: object) → bool¶
    
Compare for equality
Example
    
    >>> md = MonthDay(10, 1)
    >>> md == MonthDay(10, 1)
    True
    >>> md == MonthDay(10, 2)
    False
    
__ge__(other: MonthDay) → bool¶
    
Return self>=value.
__gt__(other: MonthDay) → bool¶
    
Return self>value.
__le__(other: MonthDay) → bool¶
    
Return self<=value.
__lt__(other: MonthDay) → bool¶
    
Return self<value.
format_iso() → str¶
    
Format as the ISO 8601 month-day format.
Inverse of `parse_iso`.
Example
    
    >>> MonthDay(10, 8).format_iso()
    '--10-08'
    
Note
This format is officially only part of the 2000 edition of the ISO 8601 standard. There is no alternative for month-day in the newer editions. However, it is still widely used in other libraries.
in_year(year: int, /) → Date¶
    
Create a date from this month-day with a given day
Example
    
    >>> MonthDay(8, 1).in_year(2025)
    Date("2025-08-01")
    
Note
This method will raise a `ValueError` if the month-day is a leap day and the year is not a leap year.
is_leap() → bool¶
    
Check if the month-day is February 29th
Example
    
    >>> MonthDay(2, 29).is_leap()
    True
    >>> MonthDay(3, 1).is_leap()
    False
    
classmethod parse_iso(s: str, /) → MonthDay¶
    
Create from the ISO 8601 format `--MM-DD` or `--MMDD`.
Inverse of `format_iso()`
Example
    
    >>> MonthDay.parse_iso("--11-23")
    MonthDay("--11-23")
    
replace(**kwargs: Any) → MonthDay¶
    
Create a new instance with the given fields replaced
Example
    
    >>> d = MonthDay(11, 23)
    >>> d.replace(month=3)
    MonthDay("--03-23")
    
class whenever.Time(hour: int = 0, minute: int = 0, second: int = 0, *, nanosecond: int = 0)¶
    
Time of day without a date component
Example
    
    >>> t = Time(12, 30, 0)
    Time(12:30:00)
    
MIDNIGHT: ClassVar[Time] = Time("00:00:00")¶
    
NOON: ClassVar[Time] = Time("12:00:00")¶
    
MAX: ClassVar[Time] = Time("23:59:59.999999999")¶
    
__eq__(other: object) → bool¶
    
Compare for equality
Example
    
    >>> t = Time(12, 30, 0)
    >>> t == Time(12, 30, 0)
    True
    >>> t == Time(12, 30, 1)
    False
    
__ge__(other: Time) → bool¶
    
Return self>=value.
__gt__(other: Time) → bool¶
    
Return self>value.
__le__(other: Time) → bool¶
    
Return self<=value.
__lt__(other: Time) → bool¶
    
Return self<value.
format_iso(*, unit: Literal['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond', 'auto'] = 'auto', basic: bool = False) → str¶
    
Format as the ISO 8601 time format.
Inverse of `parse_iso()`.
Example
    
    >>> Time(12, 30, 0).format_iso(unit='millisecond')
    '12:30:00.000'
    >>> Time(4, 0, 59, nanosecond=40_000).format_iso(basic=True)
    '040059.00004'
    
classmethod from_py_time(t: time, /) → Time¶
    
Create from a `time`
Example
    
    >>> Time.from_py_time(time(12, 30, 0))
    Time(12:30:00)
    
fold value is ignored.
on(d: Date, /) → PlainDateTime¶
    
Combine a time with a date to create a datetime
Example
    
    >>> t = Time(12, 30)
    >>> t.on(Date(2021, 1, 2))
    PlainDateTime("2021-01-02 12:30:00")
    
Then, use methods like `assume_utc()` or `assume_tz()` to find the corresponding exact time.
classmethod parse_iso(s: str, /) → Time¶
    
Create from the ISO 8601 time format
Inverse of `format_iso()`
Example
    
    >>> Time.parse_iso("12:30:00")
    Time(12:30:00)
    
py_time() → time¶
    
Convert to a standard library `time`
replace(**kwargs: Any) → Time¶
    
Create a new instance with the given fields replaced
Example
    
    >>> t = Time(12, 30, 0)
    >>> d.replace(minute=3, nanosecond=4_000)
    Time(12:03:00.000004)
    
round(unit: Literal['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'] = 'second', increment: int = 1, mode: Literal['ceil', 'floor', 'half_ceil', 'half_floor', 'half_even'] = 'half_even') → Time¶
    
Round the time to the specified unit and increment. Various rounding modes are available.
Examples
    
    >>> Time(12, 39, 59).round("minute", 15)
    Time(12:45:00)
    >>> Time(8, 9, 13).round("second", 5, mode="floor")
    Time(08:09:10)
    
### Miscellaneous¶
enum whenever.Weekday(value)¶
    
Day of the week; `.value` corresponds with ISO numbering.
Valid values are as follows:
MONDAY = <Weekday.MONDAY: 1>¶
    
TUESDAY = <Weekday.TUESDAY: 2>¶
    
WEDNESDAY = <Weekday.WEDNESDAY: 3>¶
    
THURSDAY = <Weekday.THURSDAY: 4>¶
    
FRIDAY = <Weekday.FRIDAY: 5>¶
    
SATURDAY = <Weekday.SATURDAY: 6>¶
    
SUNDAY = <Weekday.SUNDAY: 7>¶
    
whenever.MONDAY = Weekday.MONDAY¶
    
Day of the week; `.value` corresponds with ISO numbering.
whenever.TUESDAY = Weekday.TUESDAY¶
    
Day of the week; `.value` corresponds with ISO numbering.
whenever.WEDNESDAY = Weekday.WEDNESDAY¶
    
Day of the week; `.value` corresponds with ISO numbering.
whenever.THURSDAY = Weekday.THURSDAY¶
    
Day of the week; `.value` corresponds with ISO numbering.
whenever.FRIDAY = Weekday.FRIDAY¶
    
Day of the week; `.value` corresponds with ISO numbering.
whenever.SATURDAY = Weekday.SATURDAY¶
    
Day of the week; `.value` corresponds with ISO numbering.
whenever.SUNDAY = Weekday.SUNDAY¶
    
Day of the week; `.value` corresponds with ISO numbering.
exception whenever.RepeatedTime¶
    
Bases: `ValueError`
A datetime is repeated in a timezone, e.g. because of DST
exception whenever.SkippedTime¶
    
Bases: `ValueError`
A datetime is skipped in a timezone, e.g. because of DST
exception whenever.InvalidOffsetError¶
    
Bases: `ValueError`
A string has an invalid offset for the given zone
exception whenever.TimeZoneNotFoundError¶
    
Bases: `ValueError`
A timezone with the given ID was not found
class whenever.patch_current_time(dt: Instant | ZonedDateTime | OffsetDateTime, /, *, keep_ticking: bool)¶
    
Patch the current time to a fixed value (for testing purposes). Behaves as a context manager or decorator, with similar semantics to `unittest.mock.patch`.
Important
  * This function should be used only for testing purposes. It is not thread-safe or part of the stable API.
  * This function only affects whenever’s `now` functions. It does not affect the standard library’s time functions or any other libraries. Use the `time_machine` package if you also want to patch other libraries.
  * It doesn’t affect the system timezone. If you need to patch the system timezone, set the `TZ` environment variable in combination with `time.tzset`. Be aware that this only works on Unix-like systems.


Example
    
    >>> from whenever import Instant, patch_current_time
    >>> i = Instant.from_utc(1980, 3, 2, hour=2)
    >>> with patch_current_time(i, keep_ticking=False) as p:
    ...     assert Instant.now() == i
    ...     p.shift(hours=4)
    ...     assert i.now() == i.add(hours=4)
    ...
    >>> assert Instant.now() != i
    ...
    >>> @patch_current_time(i, keep_ticking=True)
    ... def test_thing(p):
    ...     assert (Instant.now() - i) < seconds(1)
    ...     p.shift(hours=8)
    ...     sleep(0.000001)
    ...     assert hours(8) < (Instant.now() - i) < hours(8.1)
    
whenever.TZPATH: tuple[str, ...]¶
    
The paths in which `whenever` will search for timezone data. By default, this is determined the same way as `zoneinfo.TZPATH`, although you can override it using `whenever.reset_tzpath()` for `whenever` specifically.
whenever.clear_tzcache(*, only_keys: Iterable[str] | None = None) → None¶
    
Clear the timezone cache. If `only_keys` is provided, only the cache for those keys will be cleared.
Caution
Calling this function may change the behavior of existing `ZonedDateTime` instances in surprising ways. Most significantly, `exact_eq()` may return `False` between two timezone instances with the same TZ ID, if this timezone definition was changed on disk.
Use this function only if you know that you need to.
Behaves similarly to `zoneinfo.ZoneInfo.clear_cache()`.
whenever.reset_tzpath(target: Iterable[str | PathLike[str]] | None = None, /) → None¶
    
Reset or set the paths in which `whenever` will search for timezone data.
It does not affect the `zoneinfo` module or other libraries.
Note
Due to caching, you may find that looking up a timezone after setting the tzpath doesn’t load the timezone data from the new path. You may need to call `clear_tzcache()` if you want to force loading all timezones from the new path. Note that clearing the cache may have unexpected side effects, however.
Behaves similarly to `zoneinfo.reset_tzpath()`
whenever.available_timezones() → set[str]¶
    
Gather the set of all available timezones.
Each call to this function will recalculate the available timezone names depending on the currently configured `TZPATH`, and the presence of the `tzdata` package.
Warning
This function may open a large number of files, since the first few bytes of timezone files must be read to determine if they are valid.
Note
This function behaves similarly to `zoneinfo.available_timezones()`, which means it ignores the “special” zones (e.g. posixrules, right/posix, etc.)
It should give the same result as `zoneinfo.available_timezones()`, unless `whenever` was configured to use a different tzpath using `reset_tzpath()`.
whenever.reset_system_tz() → None¶
    
Resets the cached system timezone to the current system timezone.
# Indices and tables¶
  * Index
  * Module Index


