# index.md
# Whenever
**Type-safe datetimes for Python that get DST right. Rust or pure Python—your choice.**
Do you cross your fingers every time you work with Python’s [`datetime`](https://docs.python.org/3/library/datetime.html#module-datetime)—hoping
that you didn’t mix naive and aware, or run into one of its
[other pitfalls](stdlib-pitfalls/index.md#datetime-pitfalls)?
```python
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!
```
*Whenever* takes the guesswork out, bringing **well-established concepts**
from modern datetime libraries in other languages to Python.
Mixing up naive and aware becomes a **type error** instead of a bug you find in
production, and DST is handled correctly in **all** arithmetic:
```python
>>> from whenever import Instant, ZonedDateTime, PlainDateTime
# The same bedtime, DST-safe: you get your full eight hours
>>> bedtime = ZonedDateTime(2023, 3, 25, 22, tz="Europe/Paris")
>>> bedtime.add(hours=8)
ZonedDateTime("2023-03-26 07:00:00+02:00[Europe/Paris]")
# Explicit, type-safe conversions
>>> bedtime.to_tz("America/New_York")
ZonedDateTime("2023-03-25 17:00:00-04:00[America/New_York]")
# A moment in time, without timezone or calendar complexity
>>> Instant.now()
Instant("2024-07-04 10:36:56Z")
# Plain (naive) datetimes are a distinct type; impossible to mix with aware
>>> PlainDateTime(2023, 3, 26, 7) < bedtime # caught by your type checker!
```
In short, it’s designed to be:
** Correct**
: Smooths over the [sharp edges](stdlib-pitfalls/index.md#datetime-pitfalls) of the standard
library—DST first among them, but far from the only one.
** Typesafe**
: Distinct types for exact and local time mean your type checker catches
what would otherwise be a production bug.
** Fast**
: In common operations, whenever is 10-100× faster than Pendulum and
Arrow—and 2-4× as fast as the standard library.
Rather not depend on a Rust extension? A pure Python version is available too.
---
Browse the sidebar to navigate the documentation, or jump directly to a topic below.
Time is easy—once you grasp the basics
The pitfalls of the standard library
Learn how to use the library effectively
Dive into practical examples
All information on classes and functions
Speed, import time, and binary size
Find answers to common questions
Overview of the pattern formatting syntax
Find code, issues, and discussions here
# changelog.md
# Changelog
## Unreleased
- Add LLM-friendly Markdown documentation, including `llms.txt` and
`llms-full.txt`.
- **Fixed**: rounding with an odd `increment` rounded away from zero one step
early in the Rust extension. For example
`TimeDelta(nanoseconds=2).round("nanosecond", increment=5, mode="half_expand")`
returned 5ns instead of 0ns.
## 0.10.5 (2026-08-07)
- Add binary wheels for Python 3.15.
- Clarified comparison between ceil/expand and floor/trunc rounding modes
in docs (Thanks to @LilyFirefly)
## 0.10.4 (2026-08-02)
**Added**
- Support addition and subtraction on itemized deltas without
a reference date(time). This operation is performed itemwise
and emits a `CalendarUnitCompositionWarning` when nonzero calendar
units are involved, since arithmetic on these units may yield
unintuitive results. Operators `+` and `-` are now also supported
with the same warning behavior.
- Support `+` and `-` operators between date(times) and itemized deltas.
- Support reflected addition (`TimeDelta + datetime`) wherever the corresponding
`datetime + TimeDelta` operation is supported.
- Added `WheneverWarning` as the base class for all warnings emitted by
whenever, allowing package-wide suppression or escalation.
- Moved the itemized delta implementation to Python. The Rust extension
imports it if needed. This reduces the binary size
and improves maintainability.
- Improve warning messages.
**Fixed**
- Fixed an overflow when validating extremely large rounding increments
in the Rust extension. They now raise `ValueError`.
## 0.10.3 (2026-07-17)
- Fixed the pure Python implementation accepting invalid basic-format times
with a separatorless fraction (e.g. `20200101` or `20103000`). These now
raise `ValueError` like the Rust extension, instead of an `AssertionError`
or silently parsing a wrong value.
Thanks to @gaoflow for the report and fix (#391).
- Fixed several ISO parser edge cases involving malformed numeric components,
empty fractions, `24:00`, trailing duration separators, and overflow in the
Rust duration parsers.
- Removed CI checks for PyPy 3.10, which is EOL.
- Parsing a non-ASCII timezone ID now raises `ValueError` instead of
`TimeZoneNotFoundError` in the pure Python version, making it consistent
with the Rust extension. Thanks to @gaoflow for the report and fix (#393)
## 0.10.2 (2026-07-06)
- Fixed an issue in the pure Python implementation where invalid format
patterns containing a trimmed-fraction (`F`) field would raise an
`AttributeError` instead of the intended `ValueError`.
Thanks to @gaoflow for this report and fix (#386).
## 0.10.1 (2026-07-03)
**Improved**
- Reduced import time by ~60% when the Rust extension is active.
This was achieved by deferring the import of several internal submodules
and timezone database setup until they are actually needed.
**Added**
- Added `"week_mon"` and `"week_sun"` as valid
units for `start_of()` and `end_of()` on `Date`, `PlainDateTime`,
`ZonedDateTime`, and `OffsetDateTime`.
**Fixed**
- Restored utility functions to `whenever.__all__` and ensured `dir(whenever)`
includes lazily loaded public attributes without importing them.
- Fixed `Date.today_in_system_tz()` in the Rust extension so it uses whenever’s
cached system timezone instead of from `datetime`.
- Fixed `ZonedDateTime.start_of()` and `end_of()` around DST transitions.
Calendar-unit boundaries are exactly 1 nanosecond before the next
`start_of()`. Sub-day units preserve the current occurrence of repeated local
times when possible, while correctly handling gaps and folds shorter than the
requested unit.
- Fixed a pure-Python regression introduced in 0.10.0 that could return incorrect
offsets for part of a year. This occurred when a timezone switched from explicitly
recorded transitions to recurring transition rules partway through that year.
This affected a handful of timezones, and in practice only when
system timezone data wasn’t present (e.g. on Windows).
The Rust extension (the default) was not affected.
A rigorous test ensures this won’t regress again in the future.
- Limit free threading support to Python 3.14 and later.
In 3.13, free threading is still experimental and not yet stable.
## 0.10.0 (2026-04-05)
A big release with several breaking changes and improvements. Highlights
are the new delta API, customizable string formatting and parsing,
and `since()`/`until()` methods for calculating differences between datetimes.
See the full list below.
**Breaking changes**
- `DateTimeDelta` and `DateDelta` have been replaced by
`ItemizedDelta` and `ItemizedDateDelta`, respectively.
The helper functions for creating calendar deltas
(`years()`, `months()`, `weeks()`, `days()`) have also been deprecated.
The new deltas are fully un-normalized,
meaning “90 minutes” and “1 hour and 30 minutes” are distinct values.
They implement the `Mapping` interface and support a rich set of operations
including `add()`, `subtract()`, `total()`, `in_units()`, `replace()`,
and `sign()`.
**Rationale**: the “partially” normalized approach was confusing to users.
A fully un-normalized approach also better fits the new API for calculating deltas
between datetimes. This approach is also more consistent with other
libraries, and allows for more control over formatting and parsing of deltas.
**Migration**:
- Replace `DateDelta(...)` with `ItemizedDateDelta(...)`.
- Replace `DateTimeDelta(...)` with `ItemizedDelta(...)`.
- Replace `years()`, `months()`, `weeks()`, `days()` helper functions
with `ItemizedDateDelta(years=...)`, etc. Or, if passing to a
datetime method, use keyword arguments directly (e.g. `dt.add(years=1, months=2)`).
- Replace `.in_months_days()` and `.in_months_days_secs_nanos()`
with `.in_units(['months', 'days'])` and `.in_units(['months', 'days', 'seconds', 'nanoseconds'])`, respectively.
- The `Date` `+`/`-` operators with `DateDelta` are deprecated;
use `add()`/`subtract()` instead.
- The `Date` `-` operator between two dates is deprecated;
use `since()` or `subtract()` instead.
- The `ignore_dst` parameter (which was used to enable DST-unsafe operations)
has been replaced by a warnings mechanism that allows users to
suppress or escalate DST-related warnings
using per-method keyword arguments or Python’s standard warning filters.
**Rationale**: The `ignore_dst` parameter was a source of confusion,
and made the `OffsetDateTime` APIs less compatible.
**Migration**:
- Replace `ignore_dst=True` with the appropriate keyword argument:
- `OffsetDateTime` methods: `stale_offset_ok=True`
- `PlainDateTime` methods: `naive_arithmetic_ok=True`
- `TimeDelta` methods: `days_assumed_24h_ok=True`
- Alternatively, use Python’s `warnings.filterwarnings()` to
suppress `StaleOffsetWarning`,
`NaiveArithmeticWarning`, or `DaysAssumed24HoursWarning`.
- `ignore_dst` is still accepted (with a deprecation warning) and
will be removed in a future release.
- Behavior of an edge case is changed: disambiguation of non-existent times
as a result of calendar arithmetic (or `replace()`) no longer tries to reuse
the previous offset. This change also fixes a rare bug in case a timezone
transition skips an entire day (like the Samoa timezone did in 2011) (#252).
**Rationale**: Unlike the case of repeated times, reusing the previous
offset for non-existent times doesn’t have the advantage of preventing
unexpected jumps in time. The new behavior is consistent with other libraries.
- Dropped Python 3.9 support
**Rationale**: Python 3.9 is EOL since October 2025.
Python 3.9 only accounts for less than 0.1% of downloads.
- Removed `format_common_iso()` and `parse_common_iso()` methods.
Use `format_iso()` and `parse_iso()` instead.
These have been deprecated since 0.9.0.
- The `round()` methods are stricter about keyword-only and positional-only arguments.
**Deprecated**
- `TimeDelta.in_hours()`, `.in_minutes()`, `.in_seconds()`,
`.in_milliseconds()`, `.in_microseconds()`, `.in_nanoseconds()`,
`.in_days_of_24h()`, and `.in_hrs_mins_secs_nanos()`.
Use `total()` or `in_units()` instead.
- `Date.days_since()` and `Date.days_until()`.
Use `since()` and `until()` with `total='days'` instead.
- `py_date()`, `py_time()`, `py_datetime()`, and `py_timedelta()`.
Use the new `to_stdlib()` method instead, which provides a
consistent name across all types.
- `from_py_date()`, `from_py_time()`, `from_py_datetime()`, and
`from_py_timedelta()`.
Use the constructor directly instead (e.g. `Date(datetime.date(...))`).
- `parse_strptime()` methods on `OffsetDateTime` and `PlainDateTime`.
Use the new `parse()` method instead.
- `ZonedDateTime.start_of_day()`.
Use `start_of("day")` instead.
**Added or improved**
- Parsing methods now accept leap seconds (second value of `60`),
normalizing them to `59`. This applies to ISO 8601, RFC 2822,
and custom format strings.
- New `ZonedDateTime.next_transition()` and `ZonedDateTime.prev_transition()` methods
for finding the next or previous UTC offset transition (e.g. DST change)
relative to the current datetime. Returns `None` for timezones without
transitions (e.g. UTC or fixed-offset).
- New `since()` and `until()` methods on `Date`, `ZonedDateTime`,
`OffsetDateTime`, and `PlainDateTime` for calculating the difference
between two values in terms of specific calendar/time units.
- New `format()` and `parse()` methods on `Date`, `Time`, `PlainDateTime`,
`OffsetDateTime`, `ZonedDateTime`, and `Instant` for custom format/parse
patterns. Example:
`Date(2024, 3, 15).format("YYYY/MM/DD")` → `"2024/03/15"`.
These types also support `__format__`, enabling f-string usage:
`f"{date:YYYY/MM/DD}"`. See the pattern format documentation for details.
- New `TimeDelta.total()` and `TimeDelta.in_units()` methods for
converting a time delta into specific units.
- New `TimeDelta.add()` and `TimeDelta.subtract()` methods. The operators
`+` and `-` were supported already, but these methods make it easier
for simple operations, as well as making the API more consistent with other classes.
- New `OffsetDateTime.assume_tz()` method for associating an offset datetime
with a timezone.
- `round()` methods now support four new rounding modes:
`trunc`, `expand`, `half_trunc`, and `half_expand`.
They also now support larger and irregular values for `increment`.
`TimeDelta.round()` now supports days and weeks as rounding units
(with a warning about 24-hour days).
- All types that have a Python standard library equivalent now also accept these
objects in the constructor. For example: `Date(datetime.date(2024, 1, 1))`.
- New `ZonedDateTime.dst_offset()`
and `ZonedDateTime.tz_abbrev()` methods for querying timezone metadata
(DST offset adjustment and timezone abbreviation).
- Warning classes (`StaleOffsetWarning`,
`NaiveArithmeticWarning`, `DaysAssumed24HoursWarning`)
and corresponding per-method keyword arguments
(`stale_offset_ok`, `naive_arithmetic_ok`,
`days_assumed_24h_ok`) for fine-grained control over DST-related warnings.
- New `IsoWeekDate` type for representing ISO calendar week dates.
Construct with `IsoWeekDate(year, week, weekday)` or parse with
`IsoWeekDate("2024-W01-1")`.
Convert from `Date` with `Date.iso_week_date()`.
- New calendar methods on `Date`: `day_of_year()`, `days_in_month()`,
`days_in_year()`, `in_leap_year()`, `next_day()`, `prev_day()`,
`nth_weekday_of_month()`, `nth_weekday()`, `start_of()`, `end_of()`.
- New calendar methods on `PlainDateTime`, `ZonedDateTime`,
`OffsetDateTime`: `day_of_year()`, `days_in_month()`,
`days_in_year()`, `in_leap_year()`, `start_of()`, `end_of()`.
- New calendar methods on `YearMonth`: `days_in_month()`,
`days_in_year()`, `in_leap_year()`.
- A huge revamp and expansion of the documentation.
The structure and navigability of API reference and overview pages
has been improved. Several new pages have been added, including:
- An explanation of the fundamental concepts of time
- An overview of Python’s datetime pitfalls
- Explanation of the rounding API
- `YearMonth`, `MonthDay`, `Weekday`, and `IsoWeekDate` are now
implemented in pure Python always, reducing the compiled extension size.
- `Instant.add/subtract` now support passing `TimeDelta` instances.
Instead of rejecting `days` and `weeks`, these methods now emit a warning
about DST issues, consistent with the behavior of `TimeDelta`.
**Fixed**
- (Pure-Python version) Fixed incorrect behavior of `<` operator between `Time`
instances if nanoseconds are involved.
## 0.9.5 (2026-01-11)
Fix issue where not all windows wheels were built and uploaded (#317)
## 0.9.4 (2025-12-14)
Added support for free-threaded Python (#166)
## 0.9.3 (2025-10-16)
Fixed incorrect offsets for some timezones before the start of their first
recorded transition (typically pre-1950) (#296)
## 0.9.2 (2025-09-29)
Methods that take an `int`, `float`, or `str` now also accept subclasses of these types.
This is consistent with the behavior of the standard library and
improves compatibility with libraries like `numpy` and `pandas` (#260)
## 0.9.1 (2025-09-28)
Added `ZonedDateTime.now_in_system_tz()` and
`ZonedDateTime.from_system_tz()` as convenience methods to ease
migration away from `SystemDateTime`.
## 0.9.0 (2025-09-25)
**Breaking Changes**
- `SystemDateTime` has been removed and merged into `ZonedDateTime`
To create a more consistent and intuitive API, the `SystemDateTime` class
has been removed. Its functionality is now fully integrated into an
enhanced `ZonedDateTime`, which now serves as the single, canonical class
for all timezone-aware datetimes, including those based on the system’s
local timezone.
**Rationale:**
The `SystemDateTime` class, while useful, created several challenges that
compromised the library’s consistency and predictability:
* Inconsistent Behavior: Methods like `replace()` and `add()` on a
`SystemDateTime` instance would use the current system timezone definition,
not necessarily the one that was active when the instance was created.
This could lead to subtle and unpredictable bugs if the system timezone
changed during the program’s execution.
* API Division: Despite having nearly identical interfaces, `SystemDateTime`
and `ZonedDateTime` were not interchangeable. A function expecting a
`ZonedDateTime` could not accept a `SystemDateTime`, forcing users to write
more complex code with `Union` type hints.
* Maintenance Overhead: Maintaining two parallel APIs for timezone-aware
datetimes led to significant code duplication and a higher maintenance
burden.
This change unifies the API by integrating system timezone support
directly into `ZonedDateTime`, providing a single, consistent way to handle
all timezone-aware datetimes. The original use cases for `SystemDateTime`
are fully supported by the improved `ZonedDateTime`.
This new, unified approach also provides two major benefits:
* Performance: Operations on a `ZonedDateTime` representing a system time are
now orders of magnitude faster than they were on the old `SystemDateTime`.
* Cross-Platform Consistency: The new `whenever.reset_system_tz()` function
provides a reliable, cross-platform way to update the library’s view of
the system timezone, replacing the previous reliance on the Unix-only
`time.tzset()`.
**Migration:**
- Replace all `SystemDateTime` with `ZonedDateTime` in all type hints.
- Replace `SystemDateTime.now()` with `ZonedDateTime.now_in_system_tz()` (whenever >=0.9.1)
or `Instant.now().to_system_tz()` (whenever <0.9.1)
- Replace `SystemDateTime(...)` constructor calls with `ZonedDateTime.from_system_tz(...)`
(whenever >=0.9.1) or `PlainDateTime(...).assume_system_tz()` (whenever <0.9.1)
- Check calls to `.to_system_tz()` and `.assume_system_tz()`: these
methods now return a `ZonedDateTime` instance. In most cases,
no code change is needed.
- Instead of `time.tzset()`, use `whenever.reset_system_tz()` to
update the system timezone (for `whenever` only).
- `ZonedDateTime` instances with a system timezone may in rare cases
not have a known IANA timezone ID (the `tz` property will be `None`).
This is an unfortunate limitation of some platforms.
Such `ZonedDateTime` instances can still be used for all operations,
and will account for DST correctly. However, these instances cannot be pickled,
and their ISO format will not be able to include the timezone ID.
**Rationale:** This is an necessary compromise for broad system timezone support.
Other libraries (and Python’s own `zoneinfo`) have similar limitations.
- The `repr()` of all classes now includes quotes: e.g. `Date("2023-10-05")`.
Since all constructors now also accept ISO 8601 strings, the `repr()` output
can be directly used as input and thus `eval(repr(obj)) == obj`.
**Rationale:** This makes the types easier to use in interactive sessions
and tests. A round-trippable `repr()` is also a common expectation for primitive types.
- Renamed `[format|parse]_common_iso` methods to `[format|parse]_iso`.
The old methods are still available (but deprecated) to ease the transition.
**Rationale:** The “common” qualifier is no longer necessary because
these methods have been expanded to handle a wider range of ISO 8601 formats.
- Removed the deprecated `local()` methods (use `to_plain()` instead).
- Removed the deprecated `instant()` method (use `to_instant()` instead).
**Improved**
- All classes can now be directly instantiated from an ISO 8601 formatted string
passed as a sole argument. For example, `Date("2023-10-05")` is equivalent to
`Date(2023, 10, 5)` (which is still supported, of course).
- Customizable ISO 8601 Formatting: The `format_iso()` methods now accept
parameters to customize the output. You can control the separator
(e.g., `'T'` or `' '`), the smallest unit (from `hour` to `nanosecond`),
and toggle the “basic” (compact) or “extended” format.
Also, the formatting is now significantly faster. Up to 5x faster for
`ZonedDateTime`, which is now 10x faster than the standard library’s `datetime.isoformat()`.
**Fixed**
- Resolved a memory leak in the Rust extension where timezone objects that
were no longer in use were not properly evicted from the cache.
- Fixed a rare bug in determining the UTC offset for times far in the future
- Fixed `PlainDateTime` constructor raising `TypeError` instead of
`ValueError` when passed invalid parameters.
- TZ IDs starting with a `./` are now properly rejected. Other path traversal
attempts were already handled correctly.
- More robust timezone refcounting in the Rust extension, preventing crashes
in rare cases (#270)
- Panics in Rust extension no longer crash the interpreter, raise `RuntimeError` instead
## 0.8.9 (2025-09-21)
- Fixed not all test files included in source distribution (#266)
- Uploaded missing Python 3.14 wheels
## 0.8.8 (2025-07-24)
- Add wheels for Python 3.14 now that its ABI is stable.
- Add a pure Python wheel so platforms without binary wheels can use
`whenever`’s pure Python version without having to go through the source
build process (#256)
## 0.8.7 (2025-07-18)
- Fix some `MIN` and `MAX` constants not documented in the API reference.
- Add `Time.MIN` alias for `Time.MIDNIGHT` for consistency (#245)
- Fix bug in rounding of midnight `ZonedDateTime` values in “ceil”/day mode (#249)
## 0.8.6 (2025-06-23)
- Improve error message of `ZonedDateTime.from_py_datetime()` in case
the datetime’s `ZoneInfo.key` is `None`.
- Fix performance regression in `Date.day_of_week()` (#244)
## 0.8.5 (2025-06-09)
- Relax build requirements. It now only depends on `setuptools_rust` if opting
to build the Rust extension (#240)
- Fixed not all Rust files included in source distribution.
- Update some outdated docstrings.
## 0.8.4 (2025-05-28)
- Fix Pydantic JSON schema generation in certain contexts,
which affected FastAPI doc generation.
## 0.8.3 (2025-05-22)
- Ensure Pydantic parsing failures of `whenever` types always result in
a proper `ValidationError`, not a `TypeError`.
## 0.8.2 (2025-05-21)
- Allow Pydantic to generate JSON schema for `whenever` types. This is
particularly useful for generating OpenAPI schemas for FastAPI.
## 0.8.1 (2025-05-21)
**New**
- Added support for Pydantic serialization/deserialization of
`whenever` types in the ISO 8601 format. This functionality is in
preview, and may be subject to change in the future. (#175)
**Fixed**
- `Weekday` enum values from the Rust extension are now pickleable.
- Solve crash if Python’s garbage collection occurs while the Rust
extension is still initializing.
- Fixed a crash in parsing malformed fractional `TimeDelta` seconds (#234)
**Improved**
- `Time.from_py()` now ignores any `tzinfo`, instead of raising an error.
- A comprehensive refactor of the Rust extension module eliminates
most unnecessary `unsafe` code, making it safer and more idiomatic.
## 0.8.0 (2025-05-01)
A big release with several improvements and breaking changes that lay
the groundwork for the eventual 1.0 release.
**Improved**
- Timezone operations in the Rust extension are now a lot faster (5-8x),
due to a new implementation replacing the use of the standard library
`zoneinfo` module. (#202)
- The `parse_common_iso()` methods support a wider range of ISO 8601
formats. See the [updated documentation](https://whenever.readthedocs.io/en/latest/reference/iso8601.html) for details.
(#204)
- Added an “examples” page to the documentation with practical snippets. (#198)
- RFC2822 parsing is now more robust and faster. (#200)
- Import speed is improved significantly for both the Rust and pure
Python versions (#228)
**Breaking changes**
- `LocalDateTime` has been renamed to `PlainDateTime`, and the `local()`
method has been renamed to `to_plain()`. The old names are still
available (but deprecated) to ease the transition.
**Rationale**: In observing adoption of the library, the term
“local” causes confusion for a number of users, since the term
“local” is so overloaded in the Python world. `PlainDateTime` is
used in Javascript’s Temporal API, and seems to resonate better with
users. See the [FAQ](https://whenever.readthedocs.io/en/latest/faq.html#why-the-name-plaindatetime)
for a detailed discussion on the name.
- Rename `instant()` method to `to_instant()`
**Rationale**: The new name is more consistent with the rest of the
API.
- Removed the `[format|parse]_rfc3339` method.
**Rationale**: The improved ISO 8601 parsing method is now RFC 3339
compatible, making this method unnecessary.
Strict RFC 3339 parsing can still be done with `strptime`, if desired
- Passing invalid timezone names now raise a
`whenever.TimeZoneNotFoundError` (subclass of `ValueError`) instead of
`zoneinfo.ZoneInfoNotFoundError` (subclass of `KeyError`).
**Rationale**: This ensures whenever is independent of the `zoneinfo`
module, and its particularities don’t leak into the `whenever` API.
- `TimeDelta.from_py_timedelta` no longer accepts `timedelta`
subclasses.
**Rationale**: timedelta subclasses (like pendulum.Duration) often add
other time components, which cannot be guaranteed to be handled
correctly.
- The `strptime` methods have been renamed `parse_strptime`,
and its `format` argument is now a keyword-only argument.
**Rationale**: This ensures all parsing methods have the `parse_` prefix,
helping in API consistency and discoverability. The keyword-only argument
helps distinguish between the format string and the string to parse.
- The `InvalidOffset` exception has been renamed `InvalidOffsetError`
**Rationale**: this more clearly indicates that this is an error condition.
See #154 for discussion.
- `SkippedTime` and `RepeatedTime` are now subclasses of `ValueError`.
**Rationale**: it ensures these exceptions can be caught together with
other exceptions like `InvalidOffsetError` and `TimeZoneNotFoundError`
during parsing.
- Whenever is no longer affected by `ZoneInfo.clear_cache()` or
`zoneinfo.reset_tzpath()`, since it now uses its own cache with
corresponding methods.
**Rationale**: This ensures whenever is independent of `zoneinfo` in
both Rust and pure Python implementations.
**Fixed**
- Improved robustness of date calculations at extreme boundaries. (#219)
- Fixed a bug in the pure-Python version of `ZonedDateTime.exact_eq()`
that could cause false positives in some cases.
- Fixed incorrect type stubs for `day_length()` and `start_of_day()`
methods.
- Corrected the description of parameters accepted by `now()`. (#213)
## 0.7.3 (2025-03-19)
- Fixed type annotations of `Weekday` enum values, so they are properly
marked as `int`.
## 0.7.2 (2025-02-25)
- Fixed `round()` method behaving incorrectly when `increment` argument
is not passed explicitly (#209)
## 0.7.1 (2025-02-24)
- `Date.add` and `Date.subtract` now support `DateDelta` to be passed as
sole positional argument. This is consistent with the behavior of
datetime classes.
- Improved performance and robustness of date calculations at extreme
boundaries
- Minor fixes to docstrings
## 0.7.0 (2025-02-20)
This release adds rounding functionality, along with a small breaking
change (see below).
**Breaking changes**
- `TimeDelta.py_timedelta()` now truncates nanoseconds to microseconds
instead of rounding them. Use the new `round()` method to customize
rounding behavior.
**Added**
- Added `round()` to all datetime, `Instant`, and `TimeDelta` classes
- Add floor division and modulo operators to `TimeDelta`
- Add `is_ambiguous()`, `day_length()` and `start_of_day()` to
`SystemDateTime`, for consistency with `ZonedDateTime`.
- Improvements to documentation
## 0.6.17 (2025-01-30)
- Added `day_length()` and `start_of_day()` methods to `ZonedDateTime`
to make it easier to work with edge cases around DST transitions, and
prepare for implementing rounding methods in the future.
- Fix cases in type stubs where positional-only arguments weren’t
marked as such
## 0.6.16 (2024-12-22)
- Fix bug in `ZonedDateTime` `repr()` that would mangle some timezone
names
- Make `disambiguate` argument optional, defaulting to `"compatible"`.
**Rationale**: This required parameter was a frequent source of
irritation for users. Although “explicit is better than implicit”,
other modern libraries and standards also choose an (implicit)
default. For those that do want to enforce explicit handling of
ambiguous times, a special stubs file or other plugin may be
introduced in the future.
- Various small fixes to the docs
## 0.6.15 (2024-12-11)
- Add `Date.days_[since|until]` methods for calculating the difference
between two dates in days only (no months or years)
- Improve docs about arithmetic rules for calendar and time units.
## 0.6.14 (2024-11-27)
- Ensure docstrings and error messages are consistent in Rust extension
as well as the pure-Python version
- Remove undocumented properties `hour/minute/etc` from `Instant` that
were accidentally left in the Rust extension.
- `exact_eq()` now also raises `TypeError` in the pure Python version
when comparing different types.
## 0.6.13 (2024-11-17)
**Added**
- Make `from_py_datetime()` on `Instant`/`OffsetDateTime` less pedantic.
They now accept any aware datetime
- New `Date.today_in_system_tz()` convenience method
**Fixed**
- Parsing UTC offsets with out-of-range minute components (e.g. `06:79`)
now raises the expected parsing failure.
- Note in `parse_rfc2822()` docstring that it doesn’t (yet) validate
the input, due to limitations in the underlying parser.
## 0.6.12 (2024-11-08)
- Fixed `format_rfc3339()` docstrings that incorrectly included a `T`
separator. Clarified that `T` can be added by using the
`format_common_iso()` method instead. (#185)
## 0.6.11 (2024-11-04)
**Added**
- Added `YearMonth` and `MonthDay` classes for working with year-month
and month-day pairs
**Fixed**
- `whenever.__version__` is now also accessible when Rust extension is
used
## 0.6.10 (2024-10-30)
**Improved**
- Improve method documentation and autocomplete support (#172, #173,
#176)
**Fixed**
- Remove lingering undocumented `offset` on `Instant`
- Fix incorrect `LocalDateTime.difference` return type annotation
## 0.6.9 (2024-09-12)
- Clarify DST-related error messages (#169)
## 0.6.8 (2024-09-05)
- Fix object deallocation bug that caused a crash in rare cases (#167)
## 0.6.7 (2024-08-06)
- Add Python 3.13 binary wheels, now that its ABI is stable
- Small improvements to import speed
## 0.6.6 (2024-07-27)
- Fix potential memory leak in `.now()` if `time-machine` is used
## 0.6.5 (2024-07-27)
- `from_timestamp` now also accepts floats, to ease porting code from
`datetime` (#159)
- Fixed incorrect fractional seconds when parsing negative values in
`from_timestamp` methods.
- Fix some places where `ValueError` was raised instead of `TypeError`
## 0.6.4 (2024-07-26)
- Add helper `patch_current_time` for patching current time in whenever
(only) (#147)
- Support patching the current time with
[time-machine](https://github.com/adamchainz/time-machine) (#147)
- Remove undocumented `year`/`month`/`day`/`offset` properties from
`Instant`
- Reduce size of binary distributions
- Clarify contribution guidelines
## 0.6.3 (2024-07-13)
- Improve robustness and speed of keyword argument parsing in Rust
extension (#149)
- Add more answers to common questions in the docs and FAQ (#148, #150)
## 0.6.2 (2024-07-04)
- Add third-party licenses to distributions
## 0.6.1 (2024-07-04)
- Small updates to project metadata
## 0.6.0 (2024-07-04)
A big release touting a Rust extension module and an API more consistent
with other modern libraries.
**Added or improved**
- Implement as a Rust extension module, leading to a big speedup
- Add `replace_date` and `replace_time` methods to datetimes.
- Add `Date.MIN` and `Date.MAX` constants.
- `from_py_*` methods are more robust.
- The pickle format for most types is now more efficient.
**Breaking changes**
- `UTCDateTime` is now `Instant`. Removed methods that were specific to
UTC.
**Rationale**: `Instant` is simpler and more conceptually clear. It
also avoids the mistake of performing calendar arithmetic in UTC.
- `NaiveDateTime` is now `LocalDateTime`
**Rationale**: “Local” is more descriptive for describing the
concept of “wall clock” time observed locally by humans. It’s also
consistent with other libraries and standards.
- Nanosecond precision is now the default for all datetimes and deltas.
`nanosecond` is a keyword-only argument for all constructors, to
prevent mistakes porting code from `datetime` (which uses
microseconds).
**Rationale**: Nanosecond precision is the standard for modern
datetime libraries.
- Unified `[from_]canonical_format` methods with `[from_]common_iso8601`
methods into `[format|parse]_common_iso` methods.
**Rationale**: This cuts down on the number of methods; the
performance benefits of separate methods aren’t worth the clutter.
- Timestamp methods now use integers instead of floats. There are now
separate methods for seconds, milliseconds, and nanoseconds.
**Rationale**: This prevents loss of precision when converting to
floats, and is more in line with other modern libraries.
- Renamed `[from_][rfc3339|rfc2822]` methods to
`[format|parse]_[rfc3339|rfc2822]`.
**Rationale**: Consistency with other methods.
- Added explicit `ignore_dst=True` flag to DST-unsafe operations such as
shifting an offset datetime.
**Rationale**: Previously, DST-unsafe operations were completely
disallowed, but to a frustrating degree. This flag is a better
alternative than having users resort to workarounds.
- Renamed `as_utc`, `as_offset`, `as_zoned`, `as_local` to `to_utc`,
`to_fixed_offset`, `to_tz`, `to_system_tz`, and the
`NaiveDateTime.assume_*` methods accordingly
**Rationale**: “to” better clarifies a conversion is being made (not
a replacement), and “fixed offset” and “tz” are more descriptive
than “offset” and “zoned”.
- `disambiguate=` is non-optional for all relevant methods. The only
exception is the constructor, which defaults to “raise”.
**Rationale**: This makes it explicit how ambiguous and non-existent
times are handled.
- Removed weakref support.
**Rationale**: The overhead of weakrefs was too high for such
primitive objects, and the use case was not clear.
- Weekdays are now an enum instead of an integer.
**Rationale**: Enums are more descriptive and less error-prone,
especially since ISO weekdays start at 1 and Python weekdays at 0.
- Calendar units in `Date[Time]Delta` can now only be retrieved
together. For example, there is no `delta.months` or `delta.days`
anymore, `delta.in_months_days()` should be used in this case.
**Rationale**: This safeguards against mistakes like
`(date1 - date2).days` which would only return the *days component* of
the delta, excluding months. Having to call `in_months_days()` is more
explicit that both parts are needed.
- Units in delta cannot be different signs anymore (after
normalization).
**Rationale**: The use case for mixed sign deltas (e.g. 2 months and
-15 days) is unclear, and having a consistent sign makes it easier to
reason about. It also aligns with the most well-known version of the
ISO format.
- Calendar units are normalized, but only in so far as they can be
converted strictly. For example, 1 year is always equal to 12 months,
but 1 month isn’t equal to a fixed number of days. Refer to the delta
docs for more information.
**Rationale**: This is more in line with `TimeDelta` which also
normalizes.
- Renamed `AmbiguousTime` to `RepeatedTime`.
**Rationale**: The new name is more descriptive for repeated times
occurring twice due to DST. It also clarifies the difference between
“repeated” times and “ambiguous” times (which can also refer to
non-existent times).
- Dropped Python 3.8 support
**Rationale**: Rust extension relies on C API features added in Python
3.9. Python 3.8 will be EOL later this year.
## 0.5.1 (2024-04-02)
- Fix `LocalSystemDateTime.now()` not setting the correct offset (#104)
## 0.5.0 (2024-03-21)
**Breaking changes**
- Fix handling of `-0000` offset in RFC2822 format, which was not
according to the standard. `NaiveDateTime` can now no longer be
created from this format.
- `DateDelta` canonical format now uses `P` prefix.
**Improved**
- Add explicit ISO8601 formatting/parsing methods to datetimes, date,
time, and deltas.
- Add missing `Date.from_canonical_format` method.
- Separate docs for deltas and datetimes.
- `NaiveDateTime.assume_offset` now also accepts integers as hour
offsets.
## 0.4.0 (2024-03-13)
A big release with the main feature being the addition of date/time
deltas. I’ve also tried to bundle as many small breaking changes as
possible into this release, to avoid having to do them in the future.
**Breaking changes**
- `LocalDateTime` renamed to `LocalSystemDateTime`.
**Rationale**: The `LocalDateTime` name is used in other libraries for
naive datetimes, and the new name is more explicit.
- `LocalSystemDateTime` no longer adjusts automatically to changes in
the system timezone. Now, `LocalSystemDateTime` reflects the system
timezone at the moment of instantiation. It can be updated explicitly.
**Rationale**: The old behavior was dependent on too many assumptions,
and behaved unintuitively in some cases. It also made the class
dependent on shared mutable state, which made it hard to reason about.
- The `disambiguate=` argument now also determines how non-existent
times are handled.
**Rationale**: This makes it possible to handle both ambiguous and
non-existent times gracefully and in a consistent way. This behavior
is also more in line with the RFC5545 standard, and Temporal.
- `from_naive()` removed in favor of methods on `NaiveDateTime`. For
example, `UTCDateTime.from_naive(n)` becomes `n.assume_utc()`.
**Rationale**: It’s shorter, and more explicit about assumptions.
- Renamed `ZonedDateTime.disambiguated()` to `.is_ambiguous()`.
**Rationale**: The new name distinguishes it from the `disambiguate=`
argument, which also affects non-existent times.
- Replaced `.py` property with `.py_datetime()` method.
**Rationale**: Although it currently works fine as a property, this
may be changed in the future if the library no longer contains a
`datetime` internally.
- Removed properties that simply delegated to the underlying `datetime`
object: `tzinfo`, `weekday`, and `fold`. `date` and `time` now return
`whenever.Date` and `whenever.Time` objects.
**Rationale**: Removing these properties makes it possible to create
improved versions. If needed, these properties can be accessed from
the underlying datetime object with `.py_datetime()`.
- Renamed `.canonical_str()` to `.canonical_format()`.
**Rationale**: A more descriptive name.
- Renamed `DoesntExistInZone` to `SkippedTime`, `Ambiguous` to
`AmbiguousTime`.
**Rationale**: The new names are shorter and more consistent.
- Renamed `min` and `max` to `MIN` and `MAX`.
**Rationale**: Consistency with other uppercase class constants
**Improved**
- Added a `disambiguation="compatible"` option that matches the behavior
of other languages and the RFC5545 standard.
- Shortened the `repr()` of all types, use space separator instead of
`T`.
- Added `sep="T" or " "` option to `canonical_format()`
- `OffsetDateTime` constructor and methods creating offset datetimes now
accept integers as hour offsets.
- Added `Date` and `Time` classes for working with dates and times
separately.
## 0.3.4 (2024-02-07)
- Improved exception messages for ambiguous or non-existent times
(#26)
## 0.3.3 (2024-02-04)
- Add CPython-maintained `tzdata` package as Windows dependency (#32)
## 0.3.2 (2024-02-03)
- Relax overly strict Python version constraint in package metadata
(#33)
## 0.3.1 (2024-02-01)
- Fix packaging metadata issue involving README and CHANGELOG being
installed in the wrong place (#23)
## 0.3.0 (2024-01-23)
**Breaking changes**
- Change pickle format so that backwards-compatible unpickling is
possible in the future.
**Added**
- Added `strptime()` to `UTCDateTime`, `OffsetDateTime` and
`NaiveDateTime`.
- Added `rfc2822()`/`from_rfc2822()` to `UTCDateTime`,
`OffsetDateTime` and `NaiveDateTime`.
- Added `rfc3339()`/`from_rfc3339()` to `UTCDateTime` and
`OffsetDateTime`
## 0.2.1 (2024-01-20)
- added `days()` timedelta alias
- Improvements to README, other docs
## 0.2.0 (2024-01-10)
**Breaking changes**
- Disambiguation of local datetimes is now consistent with zoned
datetimes, and is also run on `replace()`.
- Renamed:
- `from_str` → `from_canonical_str`
- `to_utc/offset/zoned/local` → `as_utc/offset/zoned/local`.
- `ZonedDateTime.zone` → `ZonedDateTime.tz`
**Added**
- Support comparison between all aware datetimes
- support subtraction between all aware datetimes
- Convenience methods for converting between aware/naive
- More robust handling of zoned/local edge cases
**Docs**
- Cleaned up API reference
- Added high-level overview
## 0.1.0 (2023-12-20)
- Implement `OffsetDateTime`, `ZonedDateTime` and `LocalDateTime`
## 0.0.4 (2023-11-30)
- Revert to pure Python implementation, as Rust extension
disadvantages outweigh its advantages
- Implement `NaiveDateTime`
## 0.0.3 (2023-11-16)
- Implement basic `UTCDateTime`
## 0.0.2 (2023-11-10)
- Empty release with Rust extension module
## 0.0.1
- Dummy release
# contributing.md
# Contributing
## Before you start
Contributions are welcome, but be sure to read the guidelines below first.
- Non-trivial changes should be discussed in an issue first.
This is to avoid wasted effort if the change isn’t a good fit for the project.
- Before picking up an issue, please comment on it to let others know you’re working on it.
This will help avoid duplicated effort.
## Setting up a development environment
An example of setting up things up on a Unix-like system:
```bash
# install the dependencies
make init
# build the rust extension in place (debug mode)
make build
# rebuild it in release mode
make build-release
# clear the build artifacts (useful if you want to test the pure Python version)
make clean
make test # run the tests (Python and Rust)
make fix # apply autoformatting
make ci-lint # various static checks
make typecheck # run mypy and typing tests
```
## Maintainer’s notes
Below are some points to keep in mind when making changes to the codebase:
- I purposefully opted for `pyo3_ffi` over `pyo3`. There are the main reasons:
1. The higher-level binding library PyO3 has a small additional overhead for function calls,
which can be significant for small functions. Whenever has a lot of small functions.
Only with `pyo3_ffi` can these functions be on par (or faster) than the standard library.
The overhead has decreased in recent versions of PyO3, but it’s still there.
2. I was eager to learn to use the bare C API of Python, in order to better
understand how Python extension modules and PyO3 work under the hood.
3. `whenever`’s use case is quite simple: it only contains immutable data types
with small methods. It doesn’t need the full power of PyO3.
Additional advantages of `pyo3_ffi` are:
- Its API is more stable than PyO3’s, which is still evolving.
- It allows support for per-interpreter GIL, and free-threaded Python,
which are not yet (fully) supported by PyO3.
- The tests and documentation of the Rust code are sparse. This is because
it has no public interface and is only used through its Python bindings.
You can find comprehensive tests and documentation in the Python codebase.
- To keep import time fast, some “obvious” Python modules (pathlib, re, dataclasses,
importlib.resources) are not used, or imported lazily.
- Docstrings are defined in the Python codebase, then copied to the Rust codebase using a helper script.
Synchronization is checked in CI.
- Each documentation page carries an `html_meta` description, which becomes its entry
in `llms.txt`. After changing a page, review its description and run
`make sync-llms-summaries`. Staleness is checked in CI.
# design.md
# Design philosophy
This page describes the guiding principles behind `whenever`’s API.
For concrete questions, see the [FAQ](faq.md#faq).
## Separate types for separate meanings
If two concepts carry different semantics,
they get different types—even when they look similar on the surface.
For example, a datetime with a timezone ([`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime))
and one with a fixed offset ([`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime)) both
represent a moment in time with a local clock reading,
but only the former can track DST transitions.
Encoding this distinction in the type system makes bugs that would
otherwise surface at runtime visible at development time.
This principle also extends to deltas:
an exact duration ([`TimeDelta`](reference/time_delta.md#whenever.TimeDelta)),
a bag of calendar units ([`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta)),
and a mixed bag ([`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta)) each have
different arithmetic rules.
Keeping them as separate types prevents mixing operations
that don’t make sense together.
## Footguns are flagged, not forbidden
Some operations are potential footguns—but not *always* wrong.
For example, doing arithmetic on a [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) can’t
account for DST, but may be acceptable if the user knows
DST isn’t relevant for their use case, or accepts the possibility
of an incorrect result some of the time.
Outright forbidding these operations would push users toward workarounds
that would obscure their intention. Whenever allows them but emits a
[`warning`](reference/exceptions.md#whenever.PotentialDstBugWarning),
which can then explicitly and selectively be silenced.
## No system timezone by default
Many datetime libraries silently use the system timezone as a default,
but this couples your code to the machine’s configuration—a
common source of surprises, especially in servers and containers
where the system timezone is often UTC or undefined.
In `whenever`, the system timezone is never used implicitly;
you must opt in with a dedicated method
(e.g. [`to_system_tz()`](reference/instant.md#whenever.Instant.to_system_tz),
[`assume_system_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_system_tz))
so the dependency is visible in the code.
# examples.md
# Examples
Short, copy-pasteable recipes for the things you’ll reach for most often.
For the reasoning behind them, refer to the [Guide](guide/index.md#guide).
## Get the current time in UTC
```python
>>> from whenever import Instant
>>> Instant.now()
Instant("2025-04-19 19:02:56.39569Z")
```
## Convert UTC to the system timezone
```python
>>> 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
```python
>>> 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
```python
>>> 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
```python
>>> from whenever import Date
>>> birth_date = Date(2023, 11, 2)
>>> today = Date.today_in_system_tz()
>>> today.since(birth_date, total="years")
2.3753424657534246
>>> years, months = today.since(birth_date, in_units=("years", "months")).values()
(2, 4)
```
## Assign a timezone to a datetime
```python
>>> 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
```python
>>> import datetime
>>> py_dt = datetime.datetime.now(datetime.UTC)
>>> from whenever import Instant
>>> # create an Instant from any aware datetime
>>> i = Instant(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.to_stdlib()
datetime.datetime(2025, 4, 19, 15, 2, 56, 395690, tzinfo=ZoneInfo('America/New_York'))
```
## Parse an ISO8601 datetime string
```python
>>> from whenever import Instant
>>> Instant("2025-04-19T19:02+04:00")
Instant("2025-04-19 15:02:00Z")
```
Or, if you want to keep the offset value:
```python
>>> from whenever import OffsetDateTime
>>> OffsetDateTime("2025-04-19T19:02+04:00")
OffsetDateTime("2025-04-19 19:02:00+04:00")
```
## Determine the start of the hour
```python
>>> d = ZonedDateTime.now("America/New_York")
ZonedDateTime("2025-04-19 15:46:41-04:00[America/New_York]")
>>> d.start_of("hour")
ZonedDateTime("2025-04-19 15:00:00-04:00[America/New_York]")
```
The [`start_of()`](reference/zoned_datetime.md#whenever.ZonedDateTime.start_of) method also works with
other units like `"day"`, `"minute"`, and more.
See its documentation for more details.
## Determine the end of the day
```python
>>> d = ZonedDateTime.now("America/New_York")
ZonedDateTime("2025-04-19 15:46:41-04:00[America/New_York]")
>>> d.end_of("day")
ZonedDateTime("2025-04-19 23:59:59.999999999-04:00[America/New_York]")
```
## Get the current unix timestamp
```python
>>> from whenever import Instant
>>> i = Instant.now()
>>> i.timestamp()
1745090505
```
Note that this is always in whole seconds.
If you need additional precision:
```python
>>> i.timestamp_millis()
1745090505629
>>> i.timestamp_nanos()
1745090505629346833
```
## Get a date and time from a timestamp
```python
>>> 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
```python
>>> 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
```python
>>> from whenever import Date
>>> date = Date(2023, 10, 31)
>>> date.add(months=6)
Date("2024-04-30")
```
## Discard fractional seconds
```python
>>> 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()`](reference/instant.md#whenever.Instant.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.
```python
>>> 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:
```python
>>> 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:
```python
>>> 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:
```python
>>> zoned = dt.assume_tz("Europe/Berlin", disambiguate="raise")
```
## “Same time tomorrow” across DST
Adding a day keeps the wall-clock time, even when a DST transition
makes the day shorter or longer than 24 hours:
```python
>>> from whenever import ZonedDateTime
>>> # The night before Spring Forward in Amsterdam
>>> eve = ZonedDateTime(2025, 3, 30, hour=1, tz="Europe/Amsterdam")
>>> eve.add(days=1) # same wall-clock time
ZonedDateTime("2025-03-31 01:00:00+02:00[Europe/Amsterdam]")
>>> eve.add(hours=24) # exactly 24 hours — one hour later on the clock
ZonedDateTime("2025-03-31 02:00:00+02:00[Europe/Amsterdam]")
```
## Countdown to New Year’s
```python
>>> from whenever import ZonedDateTime
>>> now = ZonedDateTime(2025, 12, 28, hour=14, tz="America/New_York")
>>> new_year = ZonedDateTime(2026, 1, 1, tz="America/New_York")
>>> days, hours = new_year.since(now, in_units=("days", "hours")).values()
(3, 10)
```
## Flight itinerary across time zones
```python
>>> from whenever import OffsetDateTime
>>> departure = OffsetDateTime(2025, 7, 1, hour=9, offset=-4) # New York
>>> arrival = OffsetDateTime(2025, 7, 1, hour=22, offset=2) # Amsterdam
>>> flight_time = arrival - departure
>>> flight_time.total("hours")
7.0
```
## Recurring monthly event
When a monthly recurrence lands on a day that doesn’t exist in the
target month, the date is truncated to the last valid day:
```python
>>> from whenever import Date
>>> meeting = Date(2025, 1, 31)
>>> meeting.add(months=1) # February doesn't have 31 days
Date("2025-02-28")
>>> meeting.add(months=2)
Date("2025-03-31")
```
## Sort a list of datetimes
All *exact types* can be compared and sorted amongst each other:
```python
>>> from whenever import Instant, ZonedDateTime, OffsetDateTime
>>> times = [
... ZonedDateTime(2025, 6, 1, hour=12, tz="Asia/Tokyo"),
... Instant.from_utc(2025, 6, 1, hour=2),
... OffsetDateTime(2025, 6, 1, hour=6, offset=4),
... ]
>>> sorted(times) # all represent the same moment—sorted by the underlying instant
[...]
```
“Plain” datetimes cannot be mixed with exact types.
This will be flagged by type checking.
## Custom format patterns
For formats beyond ISO 8601, use pattern strings:
```python
>>> from whenever import Date, PlainDateTime, OffsetDateTime
>>> Date.parse("15 Mar 2024", format="DD MMM YYYY")
Date("2024-03-15")
>>> PlainDateTime.parse("03/15/2024 02:30 PM", format="MM/DD/YYYY ii:mm aa")
PlainDateTime("2024-03-15 14:30:00")
>>> OffsetDateTime.parse("2024-03-15 14:30+02:00", format="YYYY-MM-DD hh:mmxxx")
OffsetDateTime("2024-03-15 14:30:00+02:00")
```
If your input doesn’t include an offset or timezone, parse with
[`PlainDateTime.parse()`](reference/plain_datetime.md#whenever.PlainDateTime.parse) and convert:
```python
>>> from whenever import PlainDateTime
>>> pdt = PlainDateTime.parse("2024-03-15 14:30", format="YYYY-MM-DD hh:mm")
>>> pdt.assume_utc()
Instant("2024-03-15 14:30:00Z")
```
It also integrates nicely with the standard library’s formatting protocol
(`__format__`), so you can use pattern strings in f-strings:
```python
>>> from whenever import Date
>>> d = Date(2024, 3, 15)
>>> f"{d:DD/MM/YYYY}"
'15/03/2024'
>>> f"{d}" # empty spec falls back to str()
'2024-03-15'
```
## Roundtrip: datetime → string → datetime
Every `whenever` type has an ISO-compatible reversible string representation:
```python
>>> from whenever import ZonedDateTime
>>> d = ZonedDateTime(2025, 6, 15, hour=14, minute=30, tz="Europe/Amsterdam")
>>> s = str(d)
>>> s
'2025-06-15 14:30:00+02:00[Europe/Amsterdam]'
>>> ZonedDateTime(s) == d
True
```
# faq.md
# FAQ
## Does performance really matter for a datetime library?
Most of the time, datetime handling isn’t the main bottleneck in Python
programs—but 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.
## Is free-threaded Python supported?
Yes, free-threaded Python is supported. However, this support is still
in beta. Please report any issues you encounter when using `whenever` in
a free-threaded Python environment.
## Why does [`Instant`](reference/instant.md#whenever.Instant) exist?
Since you can also express a moment in time using
[`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime), you might
wonder why [`Instant`](reference/instant.md#whenever.Instant) exists.
The reason it exists is precisely *because* it doesn’t include a
timezone. By using [`Instant`](reference/instant.md#whenever.Instant),
you clearly express that you only care about *when* something happened,
not about the local time.
Consider the difference in intent:
```python
class ChatMessage:
sent: Instant # only the moment matters
class CalendarEvent:
start: ZonedDateTime # the local time matters too
```
In the first example, it’s clear that you only care about the moment
a message was 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 doesn’t [`Instant`](reference/instant.md#whenever.Instant) have `.year`, `.hour`, etc.?
An instant represents a specific moment in time,
independent of any calendar system or timezone.
Although its debug representation uses UTC,
that’s just a convenient way to display it—it doesn’t
mean the instant *is* a UTC datetime.
```python
>>> now = Instant.now()
Instant("2026-01-23 05:30:15Z")
>>> now.year
AttributeError: 'Instant' object has no attribute 'year'
```
If you need to access calendar fields, convert to a datetime type first:
```python
>>> now.to_tz("Europe/Amsterdam").year
2026
>>> now.to_fixed_offset(0).hour # only if you truly need UTC fields
5
```
## Why does [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) exist?
Most datetime formats—ISO 8601, RFC 2822, RFC 3339—only carry a fixed
UTC offset (e.g. `+02:00`), not a full timezone name.
[`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) represents *exactly* what these formats
contain: a local time pinned to a fixed offset.
This makes it the natural choice for:
- **Parsing and serializing** timestamps from APIs, logs, and databases.
- **Representing moments in the past**, where the offset was correct
at the time of recording and the timezone rules no longer matter.
- **Simple contexts** where no DST transitions are involved.
The trade-off is that a fixed offset can’t track DST.
If you shift or round an [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime), the
library preserves the original offset verbatim—which may be wrong
for future dates if the region’s rules have changed.
That’s why these operations emit a
[`StaleOffsetWarning`](reference/exceptions.md#whenever.StaleOffsetWarning).
When you need DST-safe arithmetic, convert to
[`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) first.
See [Choosing the right type](guide/choosing-a-type.md#choosing-a-type) for guidance on which type to use.
## Why are there three delta types?
Date and time durations have fundamentally different arithmetic rules
depending on the units involved.
Rather than papering over this with a single type,
`whenever` gives each category its own type
(see [Design philosophy](design.md#design)):
1. **[`TimeDelta`](reference/time_delta.md#whenever.TimeDelta)** — for exact durations
(hours, minutes, seconds, nanoseconds).
These normalize automatically: `90 minutes` becomes `1 hour 30 minutes`.
They support comparison, mathematical operators, and don’t need
any context to resolve.
2. **[`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta)** — for pure calendar durations
(years, months, weeks, days).
These keep their components *itemized*: `1 month` stays `1 month`,
and isn’t normalized to a number of days.
Converting between calendar units requires a reference date
(because `1 month` is 28–31 days depending on when you start).
3. **[`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta)** — for mixed bags of calendar *and*
exact units, such as `1 month, 3 hours, 20 minutes`.
Useful for display and ISO 8601 round-tripping.
Like [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta), it keeps components itemized and
needs a reference date for conversion.
Having three explicit types prevents subtle bugs like comparing
`1 month` to `30 days` without context,
or accidentally normalizing away important calendar semantics.
See [Delta types](reference/deltas.md#durations) for the full reference.
## Why warnings instead of errors?
Operations that *could* introduce DST bugs—such as arithmetic on
[`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) or [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime)—emit
a warning rather than raising an exception.
This is a deliberate choice, because these operations aren’t always wrong
(see [Design philosophy](design.md#design)).
The warning approach gives you four levels of control:
1. **Learn by default** — if you’re scripting quickly, the warning appears
in the console and teaches you about the pitfall. If you’re in a hurry,
the library doesn’t get in your way and you can fix the issue later.
2. **Ban project-wide** — in production code, convert warnings to errors
with Python’s standard [`warnings`](https://docs.python.org/3/library/warnings.html#module-warnings) filter:
```python
import warnings
warnings.filterwarnings("error", category=whenever.PotentialDstBugWarning)
```
3. **Escape hatch** — when the operation is intentional, silence it with
a keyword argument (e.g. `stale_offset_ok=True`),
documenting the decision in code.
4. **Fine-grained** — configure at the module or function level, using
Python’s existing warning infrastructure.
## Why are conversions called `to_*` and `assume_*`?
When converting between types, `whenever` uses two naming conventions:
- **`to_*`** methods convert between types that already carry enough
information to determine the result unambiguously.
For example, [`ZonedDateTime.to_instant()`](reference/zoned_datetime.md#whenever.ZonedDateTime.to_instant)
can compute the exact moment because the timezone is known.
- **`assume_*`** methods convert from types that *lack* information.
The developer must supply the missing piece (a timezone, an offset).
For example, [`assume_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_tz) requires you to
specify which timezone the plain datetime is in.
The `assume_*` naming is intentional: it signals that you’re making
an assumption that the library can’t verify for you.
## 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](https://en.wikipedia.org/wiki/Civil_time). This name is most
notably used in Jiff (Rust) and Abseil (C++) libraries. While this
niche name is a boon to these languages, 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 not fully supported. 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.
However, *whenever* does accept leap seconds during parsing, normalizing
them to the previous second (59). This applies to ISO 8601, RFC 2822, and
[custom format strings](reference/pattern-format.md#pattern-format).
## 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](https://github.com/sdispater/pendulum/issues/289#issue-371964426)
with
[subclasses](https://github.com/sdispater/pendulum/issues/131#issue-241088629).
## 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.
## Is it compatible with SQLAlchemy?
Yes! Have a look at [`whenever-sqlalchemy`](https://pypi.org/project/whenever-sqlalchemy/),
a separate package that provides SQLAlchemy types and utilities for working with `whenever`.
## 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.
Installing this way is different depending on your tool of choice:
### Pip
```python3
# as a one-off command
WHENEVER_NO_BUILD_RUST_EXT=1 pip install whenever --no-binary whenever
# in requirements.txt
--no-binary whenever
whenever
```
### Poetry
```python3
# as a one-off command
WHENEVER_NO_BUILD_RUST_EXT=1 poetry run pip install --no-binary whenever whenever
# in poetry.toml (not pyproject.toml!)
[installer]
no-binary = ["whenever"]
```
### uv
```python3
# as a one-off command
uv add whenever --no-binary-package whenever
# pyproject.toml
[tool.uv]
no-binary-package = ["whenever"]
```
See [uv’s documentation](https://docs.astral.sh/uv/reference/settings/#no-binary-package) for more information
In all cases, the important part is forcing a source install so that the
Rust extension is not built.
You can check if the Rust extension is being used by running:
```bash
python -c "import whenever; print(whenever._EXTENSION_LOADED)"
```
## What about `dateutil`?
`dateutil` is more of an *extension* to `datetime` than a replacement,
so it isn’t included in the comparison with Pendulum and Arrow.
That said, while dateutil certainly provides useful helpers
(especially for parsing and arithmetic), it doesn’t address
the most fundamental issues with the standard library:
DST-safety and type-level distinction between naive and aware datetimes.
These are issues that only a full replacement can solve.
## Why not simply wrap Rust’s `jiff` library?
Jiff is a modern Rust datetime library with similar goals and
inspiration as `whenever`. There are several reasons `whenever`
doesn’t wrap it:
1. Jiff didn’t exist when `whenever` was created. Wrapping it was
only an option after most functionality was already implemented.
2. Providing a pure-Python version of `whenever` would require
re-implementing jiff’s logic in Python and keeping them 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](https://pypi.org/project/ry/).
## Why aren’t all operators supported for all types?
Some operators may be conspicuously missing for certain types, even
though they could be implemented. Whenever provides operators where their
meaning is useful and can be documented clearly.
For example, dates and datetimes support applying itemized deltas with `+`
and `-`:
```python
>>> Date(2024, 1, 31) + ItemizedDateDelta(months=1)
Date("2024-02-29")
```
These operators use the same calendar clamping rules as `add()` and
`subtract()`. As a result, adding and then subtracting the same delta is not
always reversible. Itemized deltas also support `+` and `-` with each other;
these perform field-wise composition and warn when nonzero calendar units are
involved because applying the combined delta may differ from applying its
parts sequentially.
Operators without a generally useful interpretation remain unavailable. For
example, itemized deltas cannot be multiplied or divided.
The `-` operator between two datetimes always
returns a [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta)—an exact elapsed duration where
subtraction is unambiguous.
If you need a difference in calendar units like years, months, or days,
use the [`since()`](reference/zoned_datetime.md#whenever.ZonedDateTime.since) /
[`until()`](reference/zoned_datetime.md#whenever.ZonedDateTime.until) methods instead:
```python
>>> d1 = ZonedDateTime(2020, 1, 1, tz="Europe/Amsterdam")
>>> d2 = ZonedDateTime(2023, 6, 15, tz="Europe/Amsterdam")
>>> d2 - d1 # exact elapsed time
TimeDelta("PT30263h")
>>> d2.since(d1, in_units=["years", "months", "days"]) # calendar units
ItemizedDateDelta("P3y5m14d")
```
See [Design philosophy](design.md#design) for the full rationale.
## Why can’t I subclass `whenever` classes?
`whenever` classes are marked `final` and aren’t designed for subclassing.
This is for several reasons:
1. Composition is a better way to extend the classes. Python’s dynamic
features also make it easy to create something that behaves like
a subclass.
2. Properly supporting subclassing requires a lot of extra work, and
adds subtle ways to misuse the API.
3. Enabling subclassing would undo some performance optimizations.
## Why does `.now()` have different precision on different OSes?
When you call a `.now()` method (such as [`Instant.now()`](reference/instant.md#whenever.Instant.now)), `whenever`
delegates to the underlying operating system to get the current time.
The precision of this time depends on what the OS and hardware provide:
- **Linux** typically provides nanosecond precision (9 digits).
- **macOS** typically provides microsecond precision (6 digits).
- **Windows** has its own variable precision depending on the version and configuration.
This is a limitation of the operating systems themselves, not `whenever`.
If this difference in precision causes issues in your tests or when
comparing values across different systems, you can normalize the precision
by calling `.round("microsecond")`
# fundamentals/ambiguity.md
# Ambiguity
Time zones describe how the offset from UTC *changes* over time.
When such a change occurs, local time can become ambiguous:
a given local clock reading may correspond to more than one exact time, or to none at all.
There are two ways this ambiguity appears:
- **Repeated time (a local time occurs twice).**
When clocks move backward, a range of local times is repeated.
For example, if the clock goes from `02:00` back to `01:00`, then `01:30` occurs twice:
once before the offset change and once after.
The local time alone does not tell you which exact moment is intended.
- **Skipped time (a local time does not occur).**
When clocks move forward, a range of local times is skipped entirely.
For example, if the clock jumps from `01:59` to `03:00`,
then `02:30` never occurs on that date.
Still, software must decide how to interpret such a time if it is requested.
In software, resolving the ambiguity generally comes down to a choice between two options:
should the local time be interpreted using the offset before the change,
or the offset after the change?
Even in gaps, one can extrapolate the missing local times using either side of the transition.
## The default convention
There is no natural law that dictates which choice is correct.
However, calendar standards like iCal (RFC 5545) and most mainstream date-time
libraries have converged on the same default:
ambiguous local times are resolved using the offset before the change.
This convention is not perfect, but it is consistent and predictable,
which allows higher-level operations—such as arithmetic—to behave sensibly across time zone transitions.
## Ambiguity in `whenever`
In `whenever`, ambiguous local times are by default resolved using the same convention
as most libraries: the offset before the change is used.
However, `whenever` also provides explicit options to handle ambiguity:
```python
>>> from whenever import ZonedDateTime, PlainDateTime
>>> local = PlainDateTime(2024, 10, 27, 2, 30)
>>> local.assume_tz("Europe/Amsterdam", disambiguate="earlier")
ZonedDateTime("2024-10-27 02:30:00+02:00[Europe/Amsterdam]")
>>> local.assume_tz("Europe/Amsterdam", disambiguate="later")
ZonedDateTime("2024-10-27 02:30:00+01:00[Europe/Amsterdam]")
>>> local.assume_tz("Europe/Amsterdam", disambiguate="compatible") # the default
ZonedDateTime("2024-10-27 02:30:00+02:00[Europe/Amsterdam]")
```
See [Ambiguity in timezones](guide/ambiguity.md#ambiguity) for more details on handling ambiguity in `whenever`.
# fundamentals/arithmetic.md
# Arithmetic
#### TIP
This page explains the *concepts* behind date-time arithmetic.
For how `whenever` implements these, see the [arithmetic guide](guide/arithmetic.md#arithmetic).
Arithmetic answers questions like “how many hours passed between these two events?”,
“how long ago did this happen?”, or “reschedule this event three days later.”
These operations seem simple, but their behavior depends on what the units involved actually mean.
## Two kinds of units
Date-time arithmetic uses two fundamentally different kinds of units:
* **Exact units**, such as *hours*, *minutes*, and *seconds*.
These represent fixed durations. An hour is always an hour.
* **Calendar units**, such as *days*, *weeks*, *months*, and *years*.
These are defined in terms of local dates and local clock times, not a fixed number of seconds.
This distinction is the key to understanding how arithmetic behaves around daylight saving time and other time zone transitions.
## Conventions and standards
There is no universal law that dictates how date-time arithmetic must work.
Instead, practice across many systems and applications has converged on a set
of behaviors that users find least surprising.
These expectations are captured in standards such as RFC 5545 (iCalendar)
and are followed, with minor variations, by most modern date-time libraries.
## How arithmetic is applied
Under these shared semantics:
* **Exact units are added as exact durations**
Adding two hours always advances the underlying moment by exactly two hours on the global timeline.
Daylight saving time transitions do not change the amount of time that passes.
If you ask to meet a friend “in two hours,” you expect that to mean two real hours
later—not one, and not three if a DST transition occurs in between.
* **Calendar units are added in local time**
Adding one day advances the date while keeping the local clock time the same.
If a meeting scheduled for 9:00 is moved “one day later,” it should still be at 9:00,
even if the intervening night was shorter or longer due to a daylight saving transition.
## Summary
| Unit type | Examples | What is preserved | Duration affected by DST |
|----------------|----------------------------|---------------------------|--------------------------------|
| Exact units | hours, minutes, seconds | Elapsed time | No |
| Calendar units | days, weeks, months, years | Local date and clock time | Yes |
Taken together, these rules are sometimes described as **DST-safe arithmetic**.
They aim to preserve the intent behind an operation—whether that intent is about
elapsed time or about the structure of the calendar—so that arithmetic behaves in a way that matches how people reason about time.
#### NOTE
**Days occupy a special position.** By convention (RFC 5545, and most modern
date-time libraries), days and weeks are treated as *calendar* units: adding a
day keeps the local clock time the same, even if the underlying UTC offset
changes due to DST. This is the behavior people expect when they say
“reschedule to tomorrow.”
In contexts with no local time to preserve (such as
[`Instant`](reference/instant.md#whenever.Instant) or
[`TimeDelta`](reference/time_delta.md#whenever.TimeDelta)), days can still be used but
are treated as exactly 24 hours each—with a warning to make the assumption
explicit.
# fundamentals/exact-vs-local.md
# Exact time vs local time
Many surprising behaviors in date-time code come from treating different kinds
of time as if they were the same.
Before looking at time zones, arithmetic, or edge cases,
it helps to be clear about what a time value actually represents.
The most fundamental distinction is between **exact time** and **local time**.
#### TIP
If you prefer a video explanation, [here is an excellent explanation of these concepts](https://www.youtube.com/watch?v=saeKBuPewcU).
## Exact time
An **exact time** (also called “absolute time” or “UTC time”) represents a single,
precise moment on the global timeline.
It refers to an instant that exists independently of where you are,
what time zone you are in, or how clocks are configured.
Exact time can simply be defined as time elapsed since a standard reference point,
such as the **Unix epoch**.
Examples of exact time include:
- “2026-01-15 12:00 UTC”
- “1.673.779.200 seconds since the Unix epoch”
- “The moment this database record was created”
Exact time is what you use when you care about *when something actually happened*.
It is ideal for logging, ordering events, measuring durations, and comparing timestamps.
Two exact times can always be compared, subtracted, or ordered, and the result is unambiguous.
Importantly, exact time does **not** depend on civil conventions like daylight saving time.
An hour is always an hour. If you wait two hours, two hours pass—no more, no less.
## Local time
A **local time** (also called “civil time” or “wall-clock time”) represents a clock
reading as people experience it in a particular place.
It answers questions like “What time does the clock and calendar on the wall show?”
Examples of local time include:
- “9:00 AM in Amsterdam”
- “Office hours are from 10:00 to 18:00”
- “Let’s meet tomorrow at noon”
Local time is how humans plan their days.
It aligns with calendars, business hours, and social expectations.
But local time is not inherently a single moment on the global timeline
because clocks can shift due to daylight saving time, or political decisions.
This means that “2 hours later” on the clock does not always correspond to “2 hours later” of elapsed “exact” time,
since the clock might have jumped forward or backward in the meantime.
Also, during such a jump, a local time might occur twice or not at all, creating ambiguity.
Local time only becomes meaningful when interpreted *in the context of a time zone*.
## Why this distinction matters
Many problems with date-time code come from treating exact and local time as interchangeable. They are not.
- Exact time is about *physics*: elapsed time, ordering, duration.
- Local time is about *conventions*: calendars, clocks, and human schedules.
Both are necessary. Both are useful. But they answer different kinds of questions,
and they behave differently under operations like comparison and arithmetic.
A useful mental model is this:
**exact time is what happened; local time is how we talk about it.**
## Exact and local time in `whenever`
In `whenever`, the distinction between exact and local time is made explicit through different types:
- [`Instant`](reference/instant.md#whenever.Instant) represents an exact moment on the global timeline (UTC).
- [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) represents a local clock reading without time zone context.
- [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) represents a *both* an exact moment and its local representation in a specific time zone.
More on that in the next section.
## Summary
| Concept | Exact time | Local time |
|----------------------|---------------------------------------------------------------------------|---------------------------------------------------------------------------------------|
| Represents | A precise instant on the global timeline | A human clock reading |
| Depends on time zone | No | Yes |
| Affected by DST | No | Yes |
| Typical uses | Logging, ordering, durations | Scheduling, calendars, display |
| Example | `2026-01-15T12:00:00Z` | “9:00 AM tomorrow” |
| `whenever` class | [`Instant`](reference/instant.md#whenever.Instant) | [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) |
## How time zones fit in
Exact and local time are two different ways of describing when something happens,
but they do not exist in isolation.
In real programs, we often need to move between them:
to interpret a local clock reading as a precise moment,
or to present an exact moment in a human-meaningful way.
That translation is where **time zones** come in.
Time zones define how local time relates to the global timeline—and, crucially,
how that relationship changes over time.
Understanding time zones is the next step.
# fundamentals/index.md
# The fundamentals of time
Time isn’t actually that hard—once you understand a handful of concepts.
The trouble is that most of us learn it backwards: API first.
In Python that usually means the standard library’s [`datetime`](https://docs.python.org/3/library/datetime.html#module-datetime) module,
figured out by trial and error, without ever forming a clear picture of what a
time value is supposed to represent.
It’s the same way many people use [`str`](https://docs.python.org/3/library/stdtypes.html#str) long before they’ve heard of
Unicode: a rule of thumb like *“just use UTF-8”* carries you a long way—right up
until something behaves strangely and you have no model to reason with.
*“Just use UTC”* is that kind of rule, and it fails the same way.
The pages below cover the handful of ideas such rules paper over.
They’re written to be read in order, starting with the most important
distinction of all: exact time versus local time.
A moment on the timeline, or a reading on a clock: not the same thing
The rules that connect the two—and what “timezone” actually means
When the clock says something twice, or skips it entirely
Why “a day later” and “24 hours later” are different questions
#### TIP
Once you’re comfortable with the fundamentals,
head to the [guide](guide/index.md#guide) to see how `whenever` puts them into practice.
# fundamentals/timezones.md
# Timezones
[Exact time and local time](fundamentals/exact-vs-local.md#exact-vs-local) are useful on their own, but most real programs need to move between them.
We store events as precise instants, display them to users as local clock readings,
and interpret user input as something that should happen at a specific moment.
A **time zone** describes how local time relates to exact time.
In practice, “time zone” is used to mean several different things.
Each of them captures part of that relationship, and each comes with different trade-offs.
## Offsets
The simplest way to relate local time to exact time is an **offset from UTC**,
such as `+01:00` or `-08:00`.
An offset answers a very narrow question:
*How far is local time from UTC at this moment?*
Examples:
* `2026-01-15T09:00:00+01:00`
* “This timestamp is 3 hours behind UTC”
* `Thu, 29 Jan 2026 00:03:32 +0900`
Offsets are precise and unambiguous.
Given an offset, you can always convert between local and exact time.
However, offsets are **not stable over time**.
Many regions change their offset due to daylight saving time or political decisions.
If you store only an offset, it may no longer be correct when the local rules
change or when the time is shifted into the past or future.
Offsets are excellent for *interchange*, but risky as long-term identifiers.
## Abbreviations
Time zone **abbreviations** like `PST`, `CET`, or `JST` are compact and human-friendly.
Examples:
* “The meeting is at 10:00 PST”
* “Logs are labeled in CET”
Abbreviations imply both an offset and a region,
and often suggest whether daylight saving time is in effect.
The problem is that abbreviations are **ambiguous**.
The same abbreviation can mean different things in different contexts.
For example, `CST` can refer to:
* **Central Standard Time** (North America),
* **China Standard Time**,
* or **Cuba Standard Time**
Abbreviations are useful for display purposes,
but they are a poor choice for storing or interpreting time programmatically.
## IANA time zones
The most complete way to describe the relationship between local and exact time
is using a time zone from the **[IANA Time Zone Database](https://en.wikipedia.org/wiki/Tz_database)**.
These time zones are identified uniquely by names such as `Europe/Amsterdam` or `America/Los_Angeles`.
An IANA time zone represents a *set of rules* in a *specific region*:
* how the offset from UTC changes over time
* when daylight saving transitions occur
* what those rules were in the past, and what they are expected to be in the future
Given a local time and an IANA time zone,
software can usually determine the corresponding exact time—and vice versa.
These identifiers are the closest thing we have to a “complete” time zone in software.
They are widely supported, regularly updated, and shared across programming languages and systems.
That said, they are not magical. IANA time zones can only reflect **known rules**.
If a government changes its timekeeping laws,
the database must be updated and redistributed before software can reflect the new reality.
Time zones know the future only as long as the rules stay the same.
## Choosing the right representation
None of these representations is “the true” time zone. Each answers a different question:
* Offsets describe *where local time is relative to UTC at that moment*
* Abbreviations describe *how humans commonly refer to a time in a timezone*
* IANA identifiers describe *the evolving rules of local time*
Understanding their strengths and limitations helps avoid subtle bugs and incorrect assumptions.
## Time zones in `whenever`
Whenever has two classes for dealing with time zones:
- [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) represents a local date and time with a fixed UTC offset.
It does not account for daylight saving time or historical changes,
and has a limited set of operations.
- [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) represents a local date and time in the context of an IANA time zone.
It uses the full set of rules to convert between local and exact time.
If possible, prefer [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) for most applications.
## Summary
| Representation | What it captures | Strengths | Limitations | `whenever` class |
|------------------------------|------------------------|----------------------------|-------------------------------|-----------------------------------------------------------------------------------------|
| UTC offset (`+01:00`) | Current diff. from UTC | Simple, unambiguous | May become stale when shifted | [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) |
| Abbreviation (`PST`) | Human-friendly label | Compact, readable | Ambiguous | N/A |
| IANA ID (`Europe/Amsterdam`) | Full time zone rules | Accurate, widely supported | Depends on database updates | [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) |
## What comes next: ambiguity
Time zones describe changing offsets from UTC.
When offsets change—such as during daylight saving transitions—the mapping
between local and exact time can break down.
Some local times occur **twice**. Others do not occur **at all**.
How software handles these situations is the next fundamental concept: [ambiguity](fundamentals/ambiguity.md#ambiguity2).
# guide/ambiguity.md
# Ambiguity in timezones
#### NOTE
The API for handling ambiguity is largely inspired by that of
[Temporal](https://tc39.es/proposal-temporal/docs/ambiguity.html),
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.
`whenever` allows you to customize how to handle these situations
using the `disambiguate` argument:
| `disambiguate` | Behavior in case of ambiguity |
|------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `"raise"` | Raise [`RepeatedTime`](reference/exceptions.md#whenever.RepeatedTime) or [`SkippedTime`](reference/exceptions.md#whenever.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. |
```python
>>> 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]")
>>> # --- Fold: 2:30am occurs TWICE (clocks fall back) ---
>>> # Reject ambiguous times outright
>>> 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 occurrence (summer time, +02:00)
>>> ZonedDateTime(2023, 10, 29, 2, 30, tz=paris, disambiguate="earlier")
ZonedDateTime("2023-10-29 02:30:00+02:00[Europe/Paris]")
>>> # Explicitly choose the later occurrence (winter time, +01:00)
>>> ZonedDateTime(2023, 10, 29, 2, 30, tz=paris, disambiguate="later")
ZonedDateTime("2023-10-29 02:30:00+01:00[Europe/Paris]")
>>> # Default ("compatible") picks "earlier" for folds — matching RFC 5545
>>> ZonedDateTime(2023, 10, 29, 2, 30, tz=paris)
ZonedDateTime("2023-10-29 02:30:00+02:00[Europe/Paris]")
>>> # The two occurrences are exactly 1 hour apart in real time:
>>> earlier = ZonedDateTime(2023, 10, 29, 2, 30, tz=paris, disambiguate="earlier")
>>> later = ZonedDateTime(2023, 10, 29, 2, 30, tz=paris, disambiguate="later")
>>> later - earlier
TimeDelta("PT1h")
>>> # --- Gap: 2:30am DOESN'T EXIST (clocks spring 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
>>> # "earlier" extrapolates backward → 1:30 AM (before the gap)
>>> ZonedDateTime(2023, 3, 26, 2, 30, tz=paris, disambiguate="earlier")
ZonedDateTime("2023-03-26 01:30:00+01:00[Europe/Paris]")
>>> # "later" extrapolates forward → 3:30 AM (after the gap)
>>> ZonedDateTime(2023, 3, 26, 2, 30, tz=paris, disambiguate="later")
ZonedDateTime("2023-03-26 03:30:00+02:00[Europe/Paris]")
>>> # Default ("compatible") picks "later" for gaps — matching RFC 5545
>>> ZonedDateTime(2023, 3, 26, 2, 30, tz=paris)
ZonedDateTime("2023-03-26 03:30:00+02:00[Europe/Paris]")
```
# guide/arithmetic.md
# Arithmetic
`whenever` supports differences, additions, and subtractions across all its
datetime and instant types. This page is a practical guide to those operations.
#### TIP
For the conceptual background on exact vs. calendar units,
see [the fundamentals](fundamentals/arithmetic.md#arithmetic2).
For working with duration objects directly,
see [delta types](reference/deltas.md#durations).
## Simple examples
```python
>>> ZonedDateTime("2023-12-28 11:30[Europe/Amsterdam]").add(hours=5, minutes=30)
ZonedDateTime("2023-12-28 17:00:00+01:00[Europe/Amsterdam]")
>>> Instant("2023-12-28 11:30Z") - ZonedDateTime(2023, 12, 28, tz="Europe/Amsterdam")
TimeDelta("PT12h30m")
>>> d1 = ZonedDateTime(2020, 1, 1, tz="Europe/Amsterdam")
>>> d2 = ZonedDateTime(2023, 6, 15, tz="Europe/Amsterdam")
>>> d2.since(d1, in_units=["years", "months", "days"])
ItemizedDelta("P3y5m14d")
```
## Overview
The table below summarizes which operations are available for each type.
Click a row heading to learn more about that kind of operation;
click a cell to jump to that type’s detailed section.
| | [Instant](guide/arithmetic.md#arithmetic-inst) | [ZonedDT](guide/arithmetic.md#arithmetic-zoned) | [OffsetDT](guide/arithmetic.md#arithmetic-offset) | [PlainDT](guide/arithmetic.md#arithmetic-plain) |
|-----------------------------------------------------------------------------|----------------------------------------------------------|----------------------------------------------------------|-----------------------------------------------------------|----------------------------------------------------------|
| [Difference in exact units](guide/arithmetic.md#arith-exact-diff) | [✅](guide/arithmetic.md#arithmetic-inst) | [✅](guide/arithmetic.md#arithmetic-zoned) | [✅](guide/arithmetic.md#arithmetic-offset) | [⚠️](guide/arithmetic.md#arithmetic-plain) |
| [Difference in calendar units](guide/arithmetic.md#arith-cal-diff) | [❌](guide/arithmetic.md#arithmetic-inst) | [✅](guide/arithmetic.md#arithmetic-zoned) | [✅](guide/arithmetic.md#arithmetic-offset) | [✅](guide/arithmetic.md#arithmetic-plain) |
| [Add/subtract exact units](guide/arithmetic.md#arith-add-exact) | [✅](guide/arithmetic.md#arithmetic-inst) | [✅](guide/arithmetic.md#arithmetic-zoned) | [⚠️](guide/arithmetic.md#arithmetic-offset) | [⚠️](guide/arithmetic.md#arithmetic-plain) |
| [Add/subtract calendar units](guide/arithmetic.md#arith-add-cal) | [❌](guide/arithmetic.md#arithmetic-inst) | [✅](guide/arithmetic.md#arithmetic-zoned) | [⚠️](guide/arithmetic.md#arithmetic-offset) | [✅](guide/arithmetic.md#arithmetic-plain) |
Key: ✅ fully supported · ⚠️ supported with caveats · ❌ not supported
## `-`/`difference()` vs. `since()`/`until()`
The `-` operator (and its method equivalent, `difference()`) always returns the
**exact elapsed time** as a [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta). It works between any two
exact-time types ([`Instant`](reference/instant.md#whenever.Instant), [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime),
[`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime)), which may be mixed freely:
```python
>>> d1 = ZonedDateTime(2020, 1, 1, tz="Europe/Amsterdam")
>>> d2 = ZonedDateTime(2023, 6, 15, tz="Europe/Amsterdam")
>>> d2 - d1
TimeDelta("PT30263h")
```
`since()` and `until()` are more flexible: you choose the **units** and get back
either a `float` (with `total=`) or an [`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta)
(with `in_units=`):
```python
>>> d2.since(d1, total="days") # float: calendar days
1261.0
>>> d2.since(d1, in_units=["years", "months", "days"]) # ItemizedDelta
ItemizedDelta("P3y5m14d")
```
`until()` is the direction-reversed counterpart of `since()`:
`a.until(b)` is equivalent to `b.since(a)`.
Both methods work with exact units (`hours`, `minutes`, `seconds`, `nanoseconds`)
*and* calendar units (`years`, `months`, `weeks`, `days`).
The `-` operator only returns exact elapsed time.
## Exact vs. calendar units
This section explains what the rows in the overview table mean.
For the specifics and caveats of each type, see the
[per-type sections](guide/arithmetic.md#arith-per-type) below.
### Exact units
*Exact units* — `hours`, `minutes`, `seconds`, `nanoseconds`, and sub-second
variants — represent fixed durations on the global timeline.
DST transitions never affect them: two hours is always two hours of real elapsed time:
```python
>>> d = ZonedDateTime(2023, 3, 25, hour=12, tz="Europe/Amsterdam")
>>> d.add(hours=24) # clocks spring forward overnight—local time shifts by 1 h
ZonedDateTime("2023-03-26 13:00:00+02:00[Europe/Amsterdam]")
```
[`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) has no timezone context, so exact-unit operations
emit a [`NaiveArithmeticWarning`](reference/exceptions.md#whenever.NaiveArithmeticWarning).
### Calendar units
*Calendar units* — `years`, `months`, `weeks`, `days` — measure calendar distance
and preserve the local time of day. By convention (RFC 5545), adding a day keeps the
clock at the same time, even across a DST transition:
```python
>>> d = ZonedDateTime(2023, 3, 25, hour=12, tz="Europe/Amsterdam")
>>> d.add(days=1) # "same time tomorrow"—only 23 h elapsed due to DST
ZonedDateTime("2023-03-26 12:00:00+02:00[Europe/Amsterdam]")
>>> d.add(hours=24) # exactly 24 hours later—local time shifts
ZonedDateTime("2023-03-26 13:00:00+02:00[Europe/Amsterdam]")
>>> d1 = ZonedDateTime(2020, 1, 1, tz="Europe/Amsterdam")
>>> d2 = ZonedDateTime(2023, 6, 15, tz="Europe/Amsterdam")
>>> d2.since(d1, in_units=["years", "months", "days"])
ItemizedDelta("P3y5m14d")
```
**Month truncation.** If the result falls on a day that doesn’t exist in a month,
it is truncated to the last valid day:
```python
>>> PlainDateTime(2023, 8, 31).add(months=1)
PlainDateTime("2023-09-30 00:00:00") # September has 30 days
```
Various rounding modes are available for the smallest unit in `since()`/`until()`.
See [Rounding](guide/rounding.md#rounding) for details.
#### SEE ALSO
[the fundamentals](fundamentals/arithmetic.md#arithmetic2) for the full conceptual background on exact
vs. calendar units.
## Per type
### Instant
[`Instant`](reference/instant.md#whenever.Instant) represents a single point in time with no calendar or
timezone context. It only supports exact units: `hours`, `minutes`, `seconds`, and
`nanoseconds`.
```python
>>> i = Instant("2023-03-25T12:00Z")
>>> i.add(hours=24)
Instant("2023-03-26 12:00:00Z")
>>> i2 = Instant("2023-03-28 06:00Z")
>>> i2 - i
TimeDelta("PT66h")
```
`years` and `months` are not available; `weeks` and `days`
can be treated as exact units, but emit a [`DaysAssumed24HoursWarning`](reference/exceptions.md#whenever.DaysAssumed24HoursWarning):
```python
>>> i.add(days=1) # emits DaysAssumed24HoursWarning
Instant("2023-03-26 12:00:00Z")
>>> i.add(days=1, days_assumed_24h_ok=True) # suppress
Instant("2023-03-26 12:00:00Z")
```
Becuase [`Instant`](reference/instant.md#whenever.Instant) has no calendar or timezone context,
it doesn’t support `since()`/`until()`.
Use [`in_units()`](reference/time_delta.md#whenever.TimeDelta.in_units)/[`total()`](reference/time_delta.md#whenever.TimeDelta.total)
on the result of `-`/`difference()` instead:
```python
>>> i2.difference(i).total("hours")
66.0
>>> i2.difference(i).in_units(["days", "hours"], days_assumed_24h_ok=True)
ItemizedDelta("P2dT18h")
```
### ZonedDateTime
[`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) is the recommended type for all arithmetic. It carries
full timezone rules and handles DST correctly — all four arithmetic operations are
fully supported.
```python
>>> d1 = ZonedDateTime(2020, 1, 1, tz="Europe/Amsterdam")
>>> d2 = ZonedDateTime(2023, 6, 15, tz="Europe/Amsterdam")
>>> d1.add(hours=5, minutes=30)
ZonedDateTime("2020-01-01 05:30:00+01:00[Europe/Amsterdam]")
>>> d2.since(d1, total="days")
1261.0
>>> d2.since(d1, in_units=["years", "months", "days"])
ItemizedDelta("P3y5m14d")
```
When using `since()`/`until()` with calendar units (`years`, `months`, `weeks`,
`days`), both datetimes must share the same timezone — or a [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError) is
raised. Exact units work freely across different timezones:
```python
>>> tokyo = ZonedDateTime(2023, 6, 15, tz="Asia/Tokyo")
>>> d2.since(tokyo, total="hours") # exact units: works across timezones
7.0
>>> d2.since(tokyo, total="days") # calendar units: raises ValueError
Traceback (most recent call last):
...
ValueError: Calendar units can only be used to compare ZonedDateTimes with the same timezone
```
When adding calendar units, the result may land in a DST transition.
Use `disambiguate` to control how this is resolved (default: `"compatible"`):
```python
>>> d = ZonedDateTime(2024, 10, 3, 1, 15, tz="America/Denver")
>>> d.add(months=1) # default: compatible
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'
```
The difference between `days` and `hours` is most visible during a DST transition:
```python
>>> eve = ZonedDateTime(2025, 3, 30, hour=1, tz="Europe/Amsterdam")
>>> eve.add(days=1) # "same time tomorrow"
ZonedDateTime("2025-03-31 01:00:00+02:00[Europe/Amsterdam]")
>>> eve.add(hours=24) # exactly 24 hours later
ZonedDateTime("2025-03-31 02:00:00+02:00[Europe/Amsterdam]")
```
### OffsetDateTime
[`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) carries a fixed UTC offset, not the full timezone
rules needed to determine whether DST applies at a future point. All arithmetic
operations are supported, but any operation that crosses a DST boundary may silently
carry a stale offset. These operations emit a [`StaleOffsetWarning`](reference/exceptions.md#whenever.StaleOffsetWarning):
```python
>>> d = OffsetDateTime(2024, 3, 9, 13, offset=-7)
>>> d.add(hours=24) # emits StaleOffsetWarning
OffsetDateTime("2024-03-10 13:00:00-07:00") # offset is stale; Denver is -06:00 here
>>> d.assume_tz("America/Denver").add(hours=24) # DST-safe alternative
ZonedDateTime("2024-03-10 14:00:00-06:00[America/Denver]")
>>> d.add(hours=24, stale_offset_ok=True) # suppress if intentional
OffsetDateTime("2024-03-10 13:00:00-07:00")
```
For `since()`/`until()`, calendar units (`years`, `months`, `weeks`, `days`) require
both datetimes to carry the same UTC offset — or a [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError) is raised.
Exact units work freely across different offsets:
```python
>>> d1 = OffsetDateTime("2024-06-01 10:00+00") # 10:00 UTC
>>> d2 = OffsetDateTime("2024-06-01 14:00+02") # 12:00 UTC
>>> d2.since(d1, total="hours") # exact units: works
2.0
>>> d2.since(d1, total="days") # calendar units: raises ValueError
Traceback (most recent call last):
...
ValueError: Calendar units can only be used to compare OffsetDateTimes with the same offset
```
#### ATTENTION
Even in a timezone without DST, prefer [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) for arithmetic.
Political decisions can change a region’s UTC offset in the future.
### PlainDateTime
[`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) has no timezone, so it cannot account for DST
in exact-time operations. Calendar units (`years`, `months`, `weeks`, `days`) are
fully supported without any caveats. Exact units — including the `-` operator and
`since()`/`until()` with time-of-day units — emit
[`NaiveArithmeticWarning`](reference/exceptions.md#whenever.NaiveArithmeticWarning):
```python
>>> d1 = PlainDateTime(2023, 1, 1)
>>> d2 = PlainDateTime(2023, 4, 15)
>>> d2.since(d1, in_units=["months", "days"]) # calendar: no warning
ItemizedDelta("P3m14d")
>>> d2.since(d1, total="hours") # exact: NaiveArithmeticWarning
2496.0
>>> d2.since(d1, total="hours", naive_arithmetic_ok=True) # suppress
2496.0
```
```python
>>> d = PlainDateTime(2023, 10, 29, 1, 30)
>>> d.add(hours=2) # emits NaiveArithmeticWarning
PlainDateTime("2023-10-29 03:30:00") # may not exist in your timezone
>>> d.assume_tz("Europe/Amsterdam").add(hours=2) # timezone-aware alternative
ZonedDateTime("2023-10-29 02:30:00+01:00[Europe/Amsterdam]")
>>> d.add(hours=2, naive_arithmetic_ok=True) # suppress if intentional
PlainDateTime("2023-10-29 03:30:00")
```
# guide/choosing-a-type.md
# Choosing the right type
While the standard library has a single [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime) type
for all use cases, `whenever` provides distinct types[1](#id2).
This ensures [different kinds of time](fundamentals/exact-vs-local.md#exact-vs-local) are distinguished clearly,
which helps to avoid common pitfalls when working with dates and times.
The main types are:
- [`Instant`](reference/instant.md#whenever.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`](reference/plain_datetime.md#whenever.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`](reference/zoned_datetime.md#whenever.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.
## [`Instant`](reference/instant.md#whenever.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.
```python
>>> livestream_start = Instant("2022-10-24 17:00Z")
Instant("2022-10-24 17:00:00Z")
>>> Instant.now() > livestream_start
True
>>> livestream_start.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.
## [`PlainDateTime`](reference/plain_datetime.md#whenever.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, adding exact time units to “plain” datetimes will emit a
`NaiveArithmeticWarning` to prevent you from accidentally introducing DST bugs.
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.
```python
>>> 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
# possible, but emits a warning:
>>> bus_departs.add(hours=3) # adding exact time units
# IS possible:
>>> bus_departs.add(hours=3, naive_arithmetic_ok=True) # explicitly suppress
>>> PlainDateTime(2020, 3, 15) > bus_departs # comparison with other plain datetimes
>>> 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`](reference/zoned_datetime.md#whenever.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.
```python
>>> 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`](reference/instant.md#whenever.Instant) can
be converted to a [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime).
Converting from a [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime), however,
may be [ambiguous](guide/ambiguity.md#ambiguity),
because changes to the offset can result in local times
occurring twice or not at all.
```python
>>> # 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]")
```
## [`OffsetDateTime`](reference/offset_datetime.md#whenever.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.
Like [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime), this type represents an exact time
*and* a local time. The difference is that [`OffsetDateTime`](reference/offset_datetime.md#whenever.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.
Many operations will emit a [`StaleOffsetWarning`](reference/exceptions.md#whenever.StaleOffsetWarning)
to prevent you from accidentally introducing DST bugs.
Then why use it? Firstly, most datetime formats (e.g. ISO 8601 and RFC 2822) only have fixed offsets,
making [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) ideal for representing datetimes in these formats.
Second, a [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) is simpler—so long as you
don’t need the ability to shift it. This makes [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime)
an efficient and compatible choice for representing times in the past.
```python
>>> 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
>>> # This will emit a warning!
>>> flight_arrival.add(hours=3) # a DST-bug waiting to happen!
>>> # instead:
>>> flight_arrival.add(hours=3, stale_offset_ok=True) # explicitly suppress
>>> flight_arrival.in_tz("America/New_York").add(hours=3) # use the full timezone
```
## 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 | ❌ | ❌ | ✅ | ❌ |
---
* **[1]** `java.time`, Noda Time (C#), and Temporal (JavaScript) all use a similar datamodel.
# guide/comparison.md
# Comparison and equality
All types support equality and comparison.
However, [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) instances are
never equal or comparable to the “exact” types.
## Exact time
For exact types ([`Instant`](reference/instant.md#whenever.Instant), [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime),
[`ZonedDateTime`](reference/zoned_datetime.md#whenever.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:
```python
>>> # 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()`](reference/zoned_datetime.md#whenever.ZonedDateTime.exact_eq) method.
```python
>>> 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`](reference/plain_datetime.md#whenever.PlainDateTime), equality is simply based on
whether the values are the same, since there is no concept of timezones or UTC offset:
```python
>>> 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)`](reference/zoned_datetime.md#whenever.ZonedDateTime.__eq__)
and [`PlainDateTime.__eq__`](reference/plain_datetime.md#whenever.PlainDateTime.__eq__) for more details.
## Nanosecond precision and interoperability
Take care when comparing datetimes after interoperating with databases,
the Python standard library, or other systems that may not support nanosecond precision.
Since equality is based on full nanosecond precision,
two datetimes may no longer be equal after a round-trip that loses precision.
This may not be apparent in development if your system’s clock only supports microsecond
precision (such as MacOS).
Use `.round('microsecond')` to explicitly round values to microsecond precision.
## 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](https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-strict-equality).
```python
>>> # 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
```
Unfortunately, mypy’s `--strict-equality` is *very* strict,
forcing you to match exact types exactly.
```python
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:
```python
x == OffsetDateTime(2023, 12, 28, 11, offset=1).to_instant()
```
Or annotate with a union:
```python
x: OffsetDateTime | Instant == OffsetDateTime(2023, 12, 28, 11, offset=1)
```
# guide/conversions.md
# Converting between types
## Between exact types
You can convert between exact types with the [`to_instant()`](reference/zoned_datetime.md#whenever.ZonedDateTime.to_instant),
[`to_fixed_offset()`](reference/zoned_datetime.md#whenever.ZonedDateTime.to_fixed_offset), [`to_tz()`](reference/zoned_datetime.md#whenever.ZonedDateTime.to_tz),
and [`to_system_tz()`](reference/zoned_datetime.md#whenever.ZonedDateTime.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.
```python
>>> 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 local time
Conversion to a “plain” datetime is easy: calling
[`to_plain()`](reference/zoned_datetime.md#whenever.ZonedDateTime.to_plain) simply
retrieves the date and time part of the datetime, and discards the any timezone
or offset information.
```python
>>> 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()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_utc),
[`assume_fixed_offset()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_fixed_offset),
[`assume_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_tz), and
[`assume_system_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_system_tz) methods.
```python
>>> 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]")
```
Similarly, you can associate an [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime)
with a timezone using [`assume_tz()`](reference/offset_datetime.md#whenever.OffsetDateTime.assume_tz):
```python
>>> o = OffsetDateTime(2023, 12, 28, 11, 30, offset=1)
>>> o.assume_tz("Europe/Amsterdam")
ZonedDateTime("2023-12-28 11:30:00+01:00[Europe/Amsterdam]")
```
By default, this raises an error if the offset doesn’t match the timezone.
You can control what happens in case of a mismatch with the `offset_mismatch` argument:
```python
>>> o = OffsetDateTime(2023, 12, 28, 11, 30, offset=5)
>>> o.assume_tz("Europe/Amsterdam", offset_mismatch="keep_instant")
ZonedDateTime("2023-12-28 07:30:00+01:00[Europe/Amsterdam]")
>>> o.assume_tz("Europe/Amsterdam", offset_mismatch="keep_local")
ZonedDateTime("2023-12-28 11:30:00+01:00[Europe/Amsterdam]")
```
#### TIP
The naming difference between `to_*` and `assume_*` methods is intentional.
See the [FAQ](faq.md#faq-to-vs-assume) for the rationale.
\`\`\`{admonition} When is `assume_tz` useful?
:class: hint
A common scenario is receiving timestamps from an external source
(API, database, log file) that only carries a fixed offset.
If the timezone rules for that region change later—for example,
a country abolishes DST—the stored offset may no longer be valid
for arithmetic on future dates.
[`assume_tz()`](reference/offset_datetime.md#whenever.OffsetDateTime.assume_tz) lets you associate the
correct timezone so that subsequent operations account for the
current rules.
```python3
```
# guide/deltas.md
# Working with deltas
This page gives a conceptual overview of the delta types in `whenever`.
For the full API reference, see [Delta types](reference/deltas.md#durations).
## Three types for three use cases
`whenever` provides three delta types because durations
have fundamentally different arithmetic rules depending on the units involved
(see the [FAQ](faq.md#faq-why-3-deltas) for the reasoning):
| Type | Units | When to use |
|-----------------------------------------------------------------------------------------------|----------------------------|---------------------------------------------------|
| [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) | hours, minutes, seconds, … | Measuring exact elapsed time |
| [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) | years, months, weeks, days | Calendar arithmetic (e.g. “3 months from now”) |
| [`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta) | all of the above | Display, ISO 8601 round-tripping, mixed durations |
Most of the time you won’t create delta objects directly—you’ll use
`add()`, `subtract()`, `since()`, and `until()` on datetime and date objects.
But deltas become useful when you need to *reuse* a duration, pass it around,
or inspect its components.
## Normalized vs. itemized
[`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) **normalizes** its components: `90 minutes` automatically
becomes `1 hour 30 minutes`.
This makes comparison and arithmetic straightforward.
[`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) and [`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta) keep their components
**itemized**: `1 month` stays `1 month`, not `30 days`.
This is essential because calendar units have variable lengths—a month can be
28, 29, 30, or 31 days depending on when you start.
```python
>>> TimeDelta(hours=1, minutes=90)
TimeDelta("PT2h30m") # normalized: 2 hours 30 minutes
>>> ItemizedDelta(hours=1, minutes=90)
ItemizedDelta("PT1h90m") # itemized: components kept as-is
```
## Calendar units need context
Calendar units are not fixed durations. `1 month` may be 28, 29, 30, or
31 days, and applying it can clamp at month end. As a result, calendar units
need a **reference date** for operations that convert them to other units or
combine them in a calendar-aware way.
```python
>>> d = ItemizedDateDelta(months=1)
>>> d.total("days", relative_to=Date(2024, 1, 15)) # January → February
31
>>> d.total("days", relative_to=Date(2024, 2, 15)) # February → March
29 # 2024 is a leap year
```
The same applies to [`in_units()`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta.in_units),
[`add()`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta.add), and [`subtract()`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta.subtract)
when calendar units are involved.
The same rule also means that calendar units do not reliably compose. Adding
`1 month` twice can differ from adding `2 months` once, because the first step
may change the reference date for the second step.
When you call `add()` or `subtract()` on itemized deltas **without** a
`relative_to` reference, the operation is field-wise and emits
[`CalendarUnitCompositionWarning`](reference/exceptions.md#whenever.CalendarUnitCompositionWarning) when nonzero calendar units
are involved. Exact-only composition does not warn. Field-wise composition is
literal and sometimes useful, but it should not be confused with sequential
application to a date or datetime.
For example, month-end clamping makes the two operations differ:
```python
>>> one_month = ItemizedDateDelta(months=1)
>>> start = Date("2023-01-31")
>>> start + one_month + one_month
Date("2023-03-28")
>>> # Summing fieldwise first applies two months in a single step
>>> summed = one_month + one_month # P2M
>>> start + summed
Date("2023-03-31")
```
## Balancing into different units
“Balancing” means redistributing a delta’s value across a new set of units.
Use `in_units()`:
```python
>>> td = TimeDelta(minutes=150)
>>> td.in_units(["hours", "minutes"]).values()
(2, 30)
```
For itemized deltas with calendar units, balancing requires a reference date:
```python
>>> d = ItemizedDateDelta(days=400)
>>> d.in_units(["years", "months", "days"], relative_to=Date(2024, 1, 1)).values()
(1, 1, 3)
```
## Sign
All deltas carry a single sign that applies to every component.
There are no mixed-sign deltas:
```python
>>> -ItemizedDateDelta(years=1, months=6)
ItemizedDateDelta("-P1y6m")
```
See [Sign](reference/deltas.md#delta-sign) for more details.
## Next steps
- [Arithmetic](guide/arithmetic.md#arithmetic) — adding and subtracting time from datetimes
- [Delta types](reference/deltas.md#durations) — full API reference for all three delta types
# guide/index.md
# Guide
Everything you need to use `whenever` day to day, roughly in the order
you’ll run into it. Start with [choosing a type](guide/choosing-a-type.md#choosing-a-type)—the rest
follows from that decision.
For background on dates and times in general, see the
[fundamentals](fundamentals/index.md#fundamentals).
For the details of a specific class or method, see the [API reference](reference/datetime.md#api).
## The essentials
* [Choosing the right type](guide/choosing-a-type.md)
* [Partial types](guide/partial-types.md)
* [Comparison and equality](guide/comparison.md)
* [Converting between types](guide/conversions.md)
* [Ambiguity in timezones](guide/ambiguity.md)
## Calculating with dates and times
* [Arithmetic](guide/arithmetic.md)
* [Working with deltas](guide/deltas.md)
* [Rounding](guide/rounding.md)
## Talking to the outside world
* [Formatting and parsing](guide/parsing.md)
* [Standard library conversions](guide/stdlib-convert.md)
* [The system timezone](guide/system-tz.md)
## Keeping it reliable
* [Testing](guide/testing.md)
* [Handling warnings](guide/warnings.md)
# guide/parsing.md
# Formatting and parsing
`whenever` reads and writes the standard formats: ISO 8601 as the canonical,
round-trippable representation, plus RFC 2822 for email and HTTP, and custom
patterns for everything else.
## ISO 8601
All types in *whenever* use ISO8601 as their canonical, round-trippable, string representation.
You can even instantiate objects directly from their ISO 8601 string representation:
```python
>>> Instant("2023-12-28T11Z")
Instant("2023-12-28 11:00:00Z")
>>> PlainDateTime("20231228T1130")
PlainDateTime("2023-12-28 11:30:00")
```
Below are the default string formats you get for calling each type’s
`format_iso()` method:
| Type | Default string format |
|-----------------------------------------------------------------------------------------|--------------------------------------------------------------|
| [`Instant`](reference/instant.md#whenever.Instant) | `YYYY-MM-DDTHH:MM:SSZ` |
| [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) | `YYYY-MM-DDTHH:MM:SS` |
| [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) | `YYYY-MM-DDTHH:MM:SS±HH:MM[IANA TZ ID]` [1](#id2) |
| [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) | `YYYY-MM-DDTHH:MM:SS±HH:MM` |
See the [reference documentation](reference/iso8601.md#iso8601) for more details on formatting and parsing ISO 8601 strings.
## RFC 2822
[RFC 2822](https://datatracker.ietf.org/doc/html/rfc2822.html#section-3.3) is
another common format for representing datetimes.
It’s used in email headers and HTTP headers. The format is:
```text
Weekday, DD Mon YYYY HH:MM:SS ±HHMM
```
For example: `Tue, 13 Jul 2021 09:45:00 -0900`
Use the methods [`format_rfc2822()`](reference/offset_datetime.md#whenever.OffsetDateTime.format_rfc2822) and
[`parse_rfc2822()`](reference/offset_datetime.md#whenever.OffsetDateTime.parse_rfc2822) to format and parse
to this format, respectively:
```python
>>> 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
All datetime types support custom format and parse patterns via
the `format()` and `parse()` methods.
Patterns use specifiers like `YYYY`, `MM`, `DD`, `hh`, `mm`, `ss`.
```python
>>> OffsetDateTime(2024, 3, 15, 14, 30, offset=+2).format(
... "EEE, DD MMM YYYY hh:mm:ssxxx"
... )
'Fri, 15 Mar 2024 14:30:00+02:00'
>>> Date.parse("15 Mar 2024", format="DD MMM YYYY")
Date("2024-03-15")
>>> ZonedDateTime.parse(
... "2024-03-15 14:30+01:00[Europe/Paris]",
... format="YYYY-MM-DD hh:mmxxx'['VV']'",
... )
ZonedDateTime("2024-03-15 14:30:00+01:00[Europe/Paris]")
```
See the [pattern format reference](reference/pattern-format.md#pattern-format) for the
full list of specifiers and details.
#### Deprecated
Deprecated since version 0.10.0: The `parse_strptime()` methods on `OffsetDateTime` and `PlainDateTime`
are deprecated. Use `parse()` with a pattern string instead, or convert
from a stdlib datetime:
`OffsetDateTime(datetime.strptime(...))`.
## Pydantic integration
#### WARNING
Pydantic support is still in beta and may change in the future.
`whenever` types support basic serialization and deserialization
with [Pydantic](https://docs.pydantic.dev). The behavior is identical to
the `parse_iso()` and `format_iso()` methods.
```python
>>> 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.
---
* **[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.
# guide/partial-types.md
# Partial types
Aside from the datetimes themselves, `whenever` also provides
[`Date`](reference/date.md#whenever.Date) for calendar dates and [`Time`](reference/time.md#whenever.Time) for
representing times of day.
```python
>>> 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:
```python
>>> 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 and calculating differences,
with similar semantics to modern datetime libraries:
```python
>>> d = Date(2023, 1, 31)
>>> d.add(months=1)
Date("2023-02-28")
>>> d.since(Date(2022, 10, 15), in_units=["months", "days"])
ItemizedDateDelta("P3m16d")
```
There’s also [`YearMonth`](reference/yearmonth.md#whenever.YearMonth) and [`MonthDay`](reference/monthday.md#whenever.MonthDay) for representing
year-month and month-day combinations, respectively.
These are useful for representing recurring events or birthdays.
[`IsoWeekDate`](reference/isoweekdate.md#whenever.IsoWeekDate) represents a date in the ISO 8601 week date system:
```python
>>> Date(2024, 12, 30).iso_week_date()
IsoWeekDate("2025-W01-1")
```
See the [API reference](reference/partial-types.md#partial-api) for more details.
# guide/rounding.md
# Rounding
#### NOTE
The API for rounding is largely inspired by that of Temporal (JavaScript)
It’s often useful to truncate or round a datetime or [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) 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`](reference/zoned_datetime.md#whenever.ZonedDateTime.round) method allows you to do this:
```python
>>> 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")
```
## Modes
Different rounding modes are available. They differ on two axes:
- Whether they round towards/away from zero (`trunc`/`expand`) or up/down (`ceil`/`floor`)
- How they break ties
This results in the following modes:
| Mode | Rounding direction | Tie-breaking | Examples | stdlib equivalent |
|---------------|----------------------|----------------|----------------|------------------------------------------------------------------------------------------------------------------------------------|
| `ceil` | up | n/a | 3.1→4, -3.1→-3 | [`ceil()`](https://docs.python.org/3/library/math.html#math.ceil) |
| `floor` | down | n/a | 3.1→3, -3.1→-4 | [`floor()`](https://docs.python.org/3/library/math.html#math.floor) |
| `trunc` | towards zero | n/a | 3.1→3, -3.1→-3 | [`trunc()`](https://docs.python.org/3/library/math.html#math.trunc), [`int`](https://docs.python.org/3/library/functions.html#int) |
| `expand` | away from zero | n/a | 3.1→4, -3.1→-4 | n/a |
| `half_ceil` | nearest increment | up | 3.5→4, -3.5→-3 | n/a |
| `half_floor` | nearest increment | down | 3.5→3, -3.5→-4 | n/a |
| `half_trunc` | nearest increment | towards zero | 3.5→3, -3.5→-3 | n/a |
| `half_expand` | nearest increment | away from zero | 3.5→4, -3.5→-4 | n/a |
| `half_even` | nearest increment | to even | 3.5→4, 4.5→4, | [`round()`](https://docs.python.org/3/library/functions.html#round) |
For positive values, the behavior of `ceil` is identical to `expand` and the behaviour of `floor` is identical to `trunc`.
The difference is only visible for negative values.
## Supported units
The `unit` argument allows you to specify the unit to round to.
Allowed values depend on the type of the object being rounded:
| Type | weeks | days | hours and smaller |
|------------------------------------------------------------------------------------------|------------------------|------------------------|-------------------------|
| [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) | ✅ [1](#id5) | ✅ [1](#id5) | ✅ |
| [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime), | ❌ | ✅ | ✅ |
| [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime), | ❌ | ✅ | ✅ |
| [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime), | ❌ | ✅ | ✅ |
| [`Instant`](reference/instant.md#whenever.Instant) | ❌ | ❌ [2](#id6) | ✅ |
## Increment
The `increment` argument allows you to specify the rounding increment.
For example, you can round to the nearest 15 minutes by setting `increment=15`
and `unit="minute"`.
There are some restrictions on the allowed increments:
- The increment must be a positive, non-zero integer.
- In case of rounding datetimes, the increment must be a divide a 24-hour day evenly.
For example, you can round to the nearest 90 minutes (16 increments per day),
but not to the nearest 7 seconds.
---
* **[1]** This assumes days are always 24 hours long, which is not always the case in practice due to daylight saving time changes. Thus, a [`DaysAssumed24HoursWarning`](reference/exceptions.md#whenever.DaysAssumed24HoursWarning) is issued when rounding a TimeDelta to days or weeks. Suppress it by passing `days_assumed_24h_ok=True` if you know this is acceptable for your use case: ```python >>> d = TimeDelta(hours=50) >>> d.round("day", days_assumed_24h_ok=True) TimeDelta("PT48h") ```
* **[2]** This is explicitly disallowed because an Instant has no concept of days. Treating a UTC “day” as a locally meaningful concept is a common source of bugs, so it’s better to disallow it entirely. You can still round to the nearest 24 hours by setting `unit="hour"` and `increment=24`.
# guide/stdlib-convert.md
# Standard library conversions
Most classes have an equivalent in the Python standard library.
Use the `to_stdlib()` method to convert to the standard library equivalent,
or pass the standard library object directly to the constructor:
```python
>>> from datetime import *
>>> from whenever import *
>>> py_dt = datetime(2025, 4, 19, 15, 30, tzinfo=timezone.utc)
>>> instant = Instant(py_dt)
>>> instant.to_stdlib()
datetime.datetime(2025, 4, 19, 15, 30, tzinfo=datetime.timezone.utc)
```
| *whenever* class | [`datetime`](https://docs.python.org/3/library/datetime.html#module-datetime) equivalent | *to* [`datetime`](https://docs.python.org/3/library/datetime.html#module-datetime) |
|-----------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|
| [`Instant`](reference/instant.md#whenever.Instant) | [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime) with [`UTC`](https://docs.python.org/3/library/datetime.html#datetime.UTC) | [`to_stdlib()`](reference/instant.md#whenever.Instant.to_stdlib) |
| [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) | [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime) with [`ZoneInfo`](https://docs.python.org/3/library/zoneinfo.html#zoneinfo.ZoneInfo) | [`to_stdlib()`](reference/zoned_datetime.md#whenever.ZonedDateTime.to_stdlib) |
| [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) | [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime) with [`timezone`](https://docs.python.org/3/library/datetime.html#datetime.timezone) | [`to_stdlib()`](reference/offset_datetime.md#whenever.OffsetDateTime.to_stdlib) |
| [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) | [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime) (naive) | [`to_stdlib()`](reference/plain_datetime.md#whenever.PlainDateTime.to_stdlib) |
| | | |
| [`Date`](reference/date.md#whenever.Date) | [`date`](https://docs.python.org/3/library/datetime.html#datetime.date) | [`to_stdlib()`](reference/date.md#whenever.Date.to_stdlib) |
| [`Time`](reference/time.md#whenever.Time) | [`time`](https://docs.python.org/3/library/datetime.html#datetime.time) | [`to_stdlib()`](reference/time.md#whenever.Time.to_stdlib) |
| [`YearMonth`](reference/yearmonth.md#whenever.YearMonth) | N/A | N/A |
| [`MonthDay`](reference/monthday.md#whenever.MonthDay) | N/A | N/A |
| | | |
| [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) | [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta) | [`to_stdlib()`](reference/time_delta.md#whenever.TimeDelta.to_stdlib) |
| [`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta) | N/A | N/A |
| [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) | N/A | N/A |
#### NOTE
* There are some exceptions where the conversion is not exact; see the individual method documentation for details.
* Converting to the standard library is not always lossless.
Nanoseconds will be truncated to microseconds.
* The constructor also accepts subclasses, so you can also ingest types
from `pendulum` and `arrow` libraries.
There are no Python equivalents for the following classes:
- [`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta) and [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) cannot be converted to [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta)
because they may contain calendar units,
and because they store their components in unnormalized form, unlike [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta).
- [`YearMonth`](reference/yearmonth.md#whenever.YearMonth) and [`MonthDay`](reference/monthday.md#whenever.MonthDay) cannot be converted
because there is no direct equivalent in the standard library.
# guide/system-tz.md
# 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()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_system_tz)
or [`to_system_tz()`](reference/zoned_datetime.md#whenever.ZonedDateTime.to_system_tz) methods:
```python
>>> 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
The system timezone isn’t necessarily fixed for the lifetime of a process.
`whenever` caches it the first time you access it,
which keeps behavior predictable and fast.
In the rare case that you need to change the system timezone
while your program is running, you can use the
[`reset_system_tz()`](reference/misc.md#whenever.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.
```python
>>> # 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.
```python
>>> 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:
```python
>>> d = plain.assume_system_tz()
ZonedDateTime("2024-06-04 12:00:00+02:00[]")
>>> # Correct UTC offset after adding 5 months
>>> d.add(months=5)
ZonedDateTime("2024-11-04 12:00:00+01:00[]")
```
However there are some limitations of such instances of [`ZonedDateTime`](reference/zoned_datetime.md#whenever.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 `to_stdlib()` will have a fixed offset, not a `ZoneInfo` object.
# guide/testing.md
# 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`](reference/misc.md#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`](https://github.com/adamchainz/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](https://github.com/spulec/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:
```python
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()`](reference/misc.md#whenever.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`:
```python
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!
```
# guide/warnings.md
# Handling warnings
`whenever` emits warnings when operations may produce incorrect results,
for example due to DST transitions, missing context, or field-wise
composition of calendar units. This is intentional: the operations
are classic “footguns”, but forbidding them entirely would be too strict.
Warnings are an ideal mechanism to ensure the potential issues don’t pass unnoticed.
All `whenever` warnings are subclasses of [`WheneverWarning`](reference/exceptions.md#whenever.WheneverWarning),
which is itself a subclass of Python’s built-in
[`UserWarning`](https://docs.python.org/3/library/exceptions.html#UserWarning). DST-related warnings are grouped
under [`PotentialDstBugWarning`](reference/exceptions.md#whenever.PotentialDstBugWarning). They fit into Python’s standard
[`warnings` infrastructure](https://docs.python.org/3/library/warnings.html)
fully, giving you several levels of control.
```text
UserWarning (stdlib)
└── WheneverWarning
├── CalendarUnitCompositionWarning
├── PotentialDstBugWarning
│ ├── DaysAssumed24HoursWarning
│ ├── NaiveArithmeticWarning
│ └── StaleOffsetWarning
└── WheneverDeprecationWarning
```
## Turn warnings into errors
For production code, **turn whenever’s warnings into exceptions** as early as
possible — typically in your module’s setup or at
the top of your application entry point:
```python
import warnings
import whenever
warnings.filterwarnings("error", category=whenever.WheneverWarning)
```
If you only want to target DST-related warnings:
```python
warnings.filterwarnings("error", category=whenever.PotentialDstBugWarning)
```
Any code that triggers a matching warning now raises an exception
immediately, forcing you (or your CI) to address it. This is the same principle
as `PYTHONWARNINGS=error` but scoped to `whenever`’s warning hierarchy only.
To target a specific warning type instead:
```python
# Only error on timezone-unaware arithmetic (PlainDateTime):
warnings.filterwarnings("error", category=whenever.NaiveArithmeticWarning)
# Only error on potentially stale offset operations (OffsetDateTime):
warnings.filterwarnings("error", category=whenever.StaleOffsetWarning)
```
### In pytest
When running tests, it’s highly recommended to turn `whenever` warnings into
errors so tests catch potential issues. Add this to your `pytest.ini` (or the
`[tool.pytest.ini_options]` table in `pyproject.toml`):
```ini
[pytest]
filterwarnings =
error::whenever.WheneverWarning
```
Or to target only one module of your project (leaving third-party libraries
unaffected):
```ini
[pytest]
filterwarnings =
error::whenever.WheneverWarning:mymodule.*
```
### In a specific module
You can also apply a filter at the top of a module, so it applies to all
code in that module without touching other modules:
```python
# mymodule/scheduling.py
import warnings
import whenever
warnings.filterwarnings(
"error",
category=whenever.WheneverWarning,
module=r"mymodule\.scheduling" # or re.escape(__name__)
)
```
## Suppress specific calls
Sometimes an operation is deliberately imprecise — and that’s fine, as long as
the decision is conscious and documented. Each method that may emit a
DST-related warning accepts a boolean keyword argument that suppresses it:
| Keyword argument | Suppresses | Used on |
|--------------------------------|-------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `days_assumed_24h_ok=True` | [`DaysAssumed24HoursWarning`](reference/exceptions.md#whenever.DaysAssumed24HoursWarning) | [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) methods, [`Instant`](reference/instant.md#whenever.Instant) `add`/`subtract` |
| `stale_offset_ok=True` | [`StaleOffsetWarning`](reference/exceptions.md#whenever.StaleOffsetWarning) | [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) methods |
| `naive_arithmetic_ok=True` | [`NaiveArithmeticWarning`](reference/exceptions.md#whenever.NaiveArithmeticWarning) | [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) methods |
| `cal_unit_composition_ok=True` | [`CalendarUnitCompositionWarning`](reference/exceptions.md#whenever.CalendarUnitCompositionWarning) | [`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta) and [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) `add`/`subtract` |
For example:
```python
from whenever import PlainDateTime
# Naive arithmetic is acceptable here because
next_departure = scheduled.add(hours=1, naive_arithmetic_ok=True)
```
The keyword argument documents the decision at the call site
while keeping the suppression limited to exactly one operation.
#### NOTE
These keyword arguments supersede the `ignore_dst` keyword argument
(deprecated in 0.10).
### Operators
The `+` and `-` operators always emit warnings when applicable, because
operators cannot accept keyword arguments. Use the method equivalents instead:
- `dt + delta` → `dt.add(delta, ...)`
- `dt - delta` → `dt.subtract(delta, ...)`
- `dt_a - dt_b` → `dt_a.difference(dt_b)` (for [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime),
pass `naive_arithmetic_ok=True`)
For itemized-delta composition, operators emit
[`CalendarUnitCompositionWarning`](reference/exceptions.md#whenever.CalendarUnitCompositionWarning) when either operand contains
nonzero calendar units. Exact-only composition does not warn. Use the method
form if you want to pass `cal_unit_composition_ok=True` instead of relying on
a global warning filter.
Alternatively, suppress operator warnings with Python’s standard
[`warnings.filterwarnings()`](https://docs.python.org/3/library/warnings.html#warnings.filterwarnings).
## Using Python’s warnings infrastructure
Since `whenever` warnings are standard Python warnings, you can also
suppress them with [`warnings.catch_warnings`](https://docs.python.org/3/library/warnings.html#warnings.catch_warnings):
```python
import warnings
import whenever
with warnings.catch_warnings():
warnings.simplefilter("ignore", whenever.StaleOffsetWarning)
# ... all stale-offset warnings suppressed inside this block
```
This is useful when you want to blanket-suppress warnings for a block of code
or for operators (which can’t take keyword arguments).
## Exploratory use and scripts
When hacking around or writing a quick script, you may simply want to silence
all `whenever` warnings globally and move on:
```python
import warnings
import whenever
warnings.filterwarnings("ignore", category=whenever.WheneverWarning)
```
This is fine for exploration. If you later promote the code to production,
revisit the suppressed warnings and decide for each one whether to fix the
underlying issue or suppress it explicitly with the appropriate keyword argument.
## Choosing the right approach
| Situation | Recommended approach |
|---------------------------------------|--------------------------------------------------------------------------|
| Production code | `filterwarnings("error", ...)` at startup |
| CI / test suite | `filterwarnings = error::whenever.WheneverWarning` in `pytest.ini` |
| One intentional imprecision | Per-method kwarg (e.g. `naive_arithmetic_ok=True`) + a comment |
| Suppress operator warnings | `warnings.catch_warnings()` block (Python ≥ 3.14 for concurrency safety) |
| Entire module intentionally imprecise | `filterwarnings("ignore", ..., module=r"mymodule\.*")` |
| Exploratory scripts | `filterwarnings("ignore", ...)` globally |
# performance.md
# Performance
`whenever` optimizes for three goals that are sometimes in tension:
1. **Runtime speed** — operations should be as fast as possible
2. **Import time** — `import whenever` should feel instant
3. **Package size** — the wheel should stay small for fast/slim installs
These goals can conflict: aggressive inlining improves runtime speed but
increases binary size, which in turn inflates import time.
`whenever` targets a balance, sacrificing a bit of runtime speed in cold
code paths to keep the module compact and fast to import.
---
## Runtime speed
`whenever` is compared against Python’s standard library (+ `dateutil`),
[Arrow](https://pypi.org/project/arrow/), and [Pendulum](https://pypi.org/project/pendulum/)
across common datetime operations.
*Lower is better. Bars exceeding the axis cutoff are annotated with `>`.*
Why is `whenever` faster?
- **No layering.** Arrow and Pendulum wrap Python’s `datetime.datetime` rather
than replacing it. Every operation pays the overhead of crossing extra Python
abstraction layers that `whenever` avoids entirely.
- **Optimised parsing and formatting.** The Rust extension uses hand-written,
single-pass byte-level parsers and formatters: no regex, no intermediate string
objects.
- **Front-loaded computation.** Every `ZonedDateTime` stores its UTC offset at
construction time. Operations like “normalize to UTC” or “subtract two instants”
become simple integer arithmetic with no timezone database lookup at operation
time.
- **Compiled core.** The default wheel is a Rust extension, giving C-level
performance with safe, auditable code. The pure-Python fallback still
benefits from the front-loaded computation model and outperforms Arrow on most
simple operations.
---
## Import time
`import whenever` is nearly free because the package defers all heavy
work until the first attribute access.
`import datetime` is faster on standard CPython builds because `datetime.py`
is a thin wrapper around the built-in C module `_datetime`, which imports
without loading a separate shared library. Third-party packages cannot match
that.
*Other libraries shown for context.*
Import time is mainly kept low through lazy loading of submodules and dependencies:
- The `__init__.py` uses module-level `__getattr__` (PEP 562) to defer the
extension load until first access.
Code that imports `whenever` but doesn’t use it pays essentially nothing.
- Dependencies (`datetime`, `zoneinfo`, `pydantic`, `typing`)
are imported on first use, not at module load.
---
## Package size
The chart below compares wheel sizes of `whenever` against other
datetime libraries, as well as some unrelated libraries for context.
A pure-Python wheel is also available for environments where install size
or platform coverage matters more than runtime speed.
`whenever`’s focus on runtime speed and rich API means it is relatively large.
However, it keeps the wheel size reasonable through careful design choices:
- Several types (`Weekday`, `YearMonth`, `MonthDay`, `IsoWeekDate`) are
implemented only in Python even when the extension is active, keeping the
native binary focused on the performance-critical datetime types.
- Inlining is used judiciously: hot code paths are optimized, while cold paths
are prevented from inflating the binary size.
Trade-offs not taken:
- Full, descriptive docstrings (almost 100 KB) are included in the wheel.
- Panics in Rust code are caught and converted to Python exceptions, which
requires unwinding tables and increases binary size. `orjson` compiles
with `panic = "abort"` to avoid this overhead, but `whenever` prioritizes
safety and debuggability over minimal size.
- Rust extensions have more overhead per method than C extensions,
but this is worth the safety and maintainability benefits.
---
## Running the benchmarks yourself
See `benchmarks/comparison/README.md` for setup instructions.
```shell
make bench-compare # full comparison run
make bench-compare-fast # quick comparison run
make bench-compare-docs # full run and update these charts
```
# reference/date.md
# `Date`
### *class* whenever.Date(iso_string: [str](https://docs.python.org/3/library/stdtypes.html#str),)
### *class* whenever.Date(py_date: [date](https://docs.python.org/3/library/datetime.html#datetime.date),)
### *class* whenever.Date(year: [int](https://docs.python.org/3/library/functions.html#int), month: [int](https://docs.python.org/3/library/functions.html#int), day: [int](https://docs.python.org/3/library/functions.html#int))
A date without a time component.
```pycon
>>> d = Date(2021, 1, 2)
Date("2021-01-02")
```
Can also be constructed from an ISO 8601 string
or a standard library [`date`](https://docs.python.org/3/library/datetime.html#datetime.date):
```pycon
>>> Date("2021-01-02")
Date("2021-01-02")
>>> Date(date(2021, 1, 2))
Date("2021-01-02")
```
Dates support arithmetic with [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta):
```pycon
>>> delta = Date("2021-02-28").since(Date("1994-05-15"), in_units=["years", "days"])
ItemizedDateDelta("P26y289d")
>>> Date("1994-05-15").add(delta)
Date("2021-02-28")
```
Dates can be compared and sorted:
```pycon
>>> Date(2021, 1, 2) > Date(2021, 1, 1)
True
```
#### *classmethod* from_py_date(d: [date](https://docs.python.org/3/library/datetime.html#datetime.date),) → [Date](reference/date.md#whenever.Date)
Create from a [`date`](https://docs.python.org/3/library/datetime.html#datetime.date)
```pycon
>>> Date.from_py_date(date(2021, 1, 2))
Date("2021-01-02")
```
#### Deprecated
Deprecated since version 0.10.0: Use the constructor `Date(d)` instead.
#### *classmethod* parse(s: [str](https://docs.python.org/3/library/stdtypes.html#str), , , format: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [Date](reference/date.md#whenever.Date)
Parse a date from a custom pattern string.
See [Pattern format](reference/pattern-format.md#pattern-format) for details.
```pycon
>>> Date.parse("2024/03/15", format="YYYY/MM/DD")
Date("2024-03-15")
>>> Date.parse("15 Mar 2024", format="DD MMM YYYY")
Date("2024-03-15")
```
#### *classmethod* parse_iso(s: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [Date](reference/date.md#whenever.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()`](reference/date.md#whenever.Date.format_iso)
```pycon
>>> Date.parse_iso("2021-01-02")
Date("2021-01-02")
```
#### *classmethod* today_in_system_tz() → [Date](reference/date.md#whenever.Date)
Get the current date in the system’s local timezone.
Alias for `Instant.now().to_system_tz().date()`.
```pycon
>>> Date.today_in_system_tz()
Date("2021-01-02")
```
#### \_\_add_\_(p: [DateDelta](reference/deprecated.md#whenever.DateDelta)) → [Date](reference/date.md#whenever.Date)
Add a delta to a date.
Behaves the same as [`add()`](reference/date.md#whenever.Date.add)
#### Deprecated
Deprecated since version 0.10.0: Using the `+` operator on [`Date`](reference/date.md#whenever.Date) is deprecated;
use the [`add()`](reference/date.md#whenever.Date.add) method instead.
#### \_\_eq_\_(other: [object](https://docs.python.org/3/library/functions.html#object)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare for equality
```pycon
>>> d = Date(2021, 1, 2)
>>> d == Date(2021, 1, 2)
True
>>> d == Date(2021, 1, 3)
False
```
#### \_\_format_\_(spec: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [str](https://docs.python.org/3/library/stdtypes.html#str)
Default object formatter.
Return str(self) if format_spec is empty. Raise TypeError otherwise.
#### \_\_ge_\_(other: [Date](reference/date.md#whenever.Date)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>=value.
#### \_\_gt_\_(other: [Date](reference/date.md#whenever.Date)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>value.
#### \_\_le_\_(other: [Date](reference/date.md#whenever.Date)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self<=value.
#### \_\_lt_\_(other: [Date](reference/date.md#whenever.Date)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>> Date(2021, 1, 2).format_iso()
'2021-01-02'
>>> Date(1992, 9, 4).format_iso(basic=True)
'19920904'
```
#### \_\_sub_\_(d: [DateDelta](reference/deprecated.md#whenever.DateDelta)) → [Date](reference/date.md#whenever.Date)
#### \_\_sub_\_(d: [Date](reference/date.md#whenever.Date)) → [DateDelta](reference/deprecated.md#whenever.DateDelta)
Subtract a delta from a date, or subtract two dates
Subtracting a delta works the same as [`subtract()`](reference/date.md#whenever.Date.subtract).
```pycon
>>> 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:
```pycon
>>> delta = d1 - d2
>>> d2 + delta == d1 # always
```
The following is not always true:
```pycon
>>> d1 - (d1 - d2) == d2 # not always true!
>>> -(d2 - d1) == d1 - d2 # not always true!
```
```pycon
>>> 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)
```
#### Deprecated
Deprecated since version 0.10.0: Using the `-` operator on [`Date`](reference/date.md#whenever.Date) is deprecated;
use the [`subtract()`](reference/date.md#whenever.Date.subtract) method or the [`since()`](reference/date.md#whenever.Date.since) method instead.
#### add(delta: [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) | [DateDelta](reference/deprecated.md#whenever.DateDelta),) → [Date](reference/date.md#whenever.Date)
#### add(, years: [int](https://docs.python.org/3/library/functions.html#int) = ..., months: [int](https://docs.python.org/3/library/functions.html#int) = ..., weeks: [int](https://docs.python.org/3/library/functions.html#int) = ..., days: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [Date](reference/date.md#whenever.Date)
Add a components to a date.
See [the docs on arithmetic](guide/arithmetic.md#arithmetic) for more information.
```pycon
>>> 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](reference/time.md#whenever.Time),) → [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)
Combine a date with a time to create a datetime
```pycon
>>> d = Date(2021, 1, 2)
>>> d.at(Time(12, 30))
PlainDateTime("2021-01-02 12:30:00")
```
You can use methods like [`assume_utc()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_utc)
or [`assume_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_tz) to find the corresponding exact time.
#### day_of_week() → [Weekday](reference/other-types.md#whenever.Weekday)
The day of the week
```pycon
>>> Date(2021, 1, 2).day_of_week()
Weekday.SATURDAY
>>> Weekday.SATURDAY.value
6 # the ISO value
```
#### day_of_year() → [int](https://docs.python.org/3/library/functions.html#int)
Ordinal day in the year (1–366)
```pycon
>>> Date(2021, 1, 2).day_of_year()
2
>>> Date(2021, 12, 31).day_of_year()
365
```
#### days_in_month() → [int](https://docs.python.org/3/library/functions.html#int)
Number of days in the current month (28–31)
```pycon
>>> Date(2024, 2, 1).days_in_month()
29
>>> Date(2023, 2, 1).days_in_month()
28
```
#### days_in_year() → [int](https://docs.python.org/3/library/functions.html#int)
Number of days in the current year (365 or 366)
```pycon
>>> Date(2024, 1, 1).days_in_year()
366
>>> Date(2023, 1, 1).days_in_year()
365
```
#### days_since(other: [Date](reference/date.md#whenever.Date),) → [int](https://docs.python.org/3/library/functions.html#int)
Calculate the number of days this day is after another date.
#### Deprecated
Deprecated since version 0.10.0: Use [`since()`](reference/date.md#whenever.Date.since) with unit=”days” instead.
#### days_until(other: [Date](reference/date.md#whenever.Date),) → [int](https://docs.python.org/3/library/functions.html#int)
Calculate the number of days from this date to another date.
#### Deprecated
Deprecated since version 0.10.0: Use [`until()`](reference/date.md#whenever.Date.until) with unit=”days” instead.
#### end_of(unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['year', 'month', 'week_mon', 'week_sun'],) → [Date](reference/date.md#whenever.Date)
The end of the given calendar unit
```pycon
>>> Date(2024, 8, 15).end_of("year")
Date("2024-12-31")
>>> Date(2024, 8, 15).end_of("month")
Date("2024-08-31")
>>> Date(2024, 8, 15).end_of("week_mon")
Date("2024-08-18")
```
See also [`start_of()`](reference/date.md#whenever.Date.start_of)
#### format(pattern: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [str](https://docs.python.org/3/library/stdtypes.html#str)
Format as a custom pattern string.
See [Pattern format](reference/pattern-format.md#pattern-format) for details.
```pycon
>>> Date(2024, 3, 15).format("YYYY/MM/DD")
'2024/03/15'
>>> Date(2024, 3, 15).format("DD MMM YYYY")
'15 Mar 2024'
```
#### format_iso(, basic: [bool](https://docs.python.org/3/library/functions.html#bool) = False) → [str](https://docs.python.org/3/library/stdtypes.html#str)
Format as the ISO 8601 date format.
Inverse of [`parse_iso()`](reference/date.md#whenever.Date.parse_iso).
```pycon
>>> Date(2021, 1, 2).format_iso()
'2021-01-02'
>>> Date(1992, 9, 4).format_iso(basic=True)
'19920904'
```
#### in_leap_year() → [bool](https://docs.python.org/3/library/functions.html#bool)
Whether this date’s year is a leap year
```pycon
>>> Date(2024, 1, 1).in_leap_year()
True
>>> Date(2023, 1, 1).in_leap_year()
False
```
#### iso_week_date() → [IsoWeekDate](reference/isoweekdate.md#whenever.IsoWeekDate)
The ISO week date for this date
```pycon
>>> Date(2024, 12, 30).iso_week_date()
IsoWeekDate("2025-W01-1")
```
#### month_day() → [MonthDay](reference/monthday.md#whenever.MonthDay)
The month and day (without a year component)
```pycon
>>> Date(2021, 1, 2).month_day()
MonthDay("--01-02")
```
#### next_day() → [Date](reference/date.md#whenever.Date)
The date immediately following
```pycon
>>> Date(2021, 1, 2).next_day()
Date("2021-01-03")
```
#### nth_weekday(n: [int](https://docs.python.org/3/library/functions.html#int), weekday: [Weekday](reference/other-types.md#whenever.Weekday),) → [Date](reference/date.md#whenever.Date)
The n-th occurrence of a weekday from this date (exclusive).
Negative `n` searches backward.
`n=0` raises [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError).
```pycon
>>> Date(2024, 8, 1).nth_weekday(1, Weekday.FRIDAY)
Date("2024-08-02")
>>> Date(2024, 8, 1).nth_weekday(-1, Weekday.WEDNESDAY)
Date("2024-07-31")
```
#### nth_weekday_of_month(n: [int](https://docs.python.org/3/library/functions.html#int), weekday: [Weekday](reference/other-types.md#whenever.Weekday),) → [Date](reference/date.md#whenever.Date)
The n-th occurrence of a weekday in this date’s month.
Negative `n` counts from the end.
`n=0` raises [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError).
```pycon
>>> Date(2024, 8, 1).nth_weekday_of_month(2, Weekday.FRIDAY)
Date("2024-08-09")
>>> Date(2024, 8, 1).nth_weekday_of_month(-1, Weekday.FRIDAY)
Date("2024-08-30")
```
#### prev_day() → [Date](reference/date.md#whenever.Date)
The date immediately preceding
```pycon
>>> Date(2021, 1, 2).prev_day()
Date("2021-01-01")
```
#### py_date() → [date](https://docs.python.org/3/library/datetime.html#datetime.date)
Convert to a standard library [`date`](https://docs.python.org/3/library/datetime.html#datetime.date)
#### Deprecated
Deprecated since version 0.10.0: Use [`to_stdlib()`](reference/date.md#whenever.Date.to_stdlib) instead.
#### replace(year: [int](https://docs.python.org/3/library/functions.html#int) = ..., month: [int](https://docs.python.org/3/library/functions.html#int) = ..., day: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [Date](reference/date.md#whenever.Date)
Create a new instance with the given fields replaced
```pycon
>>> d = Date(2021, 1, 2)
>>> d.replace(day=4)
Date("2021-01-04")
```
#### since(b: [Date](reference/date.md#whenever.Date), , , total: [DateDeltaUnitStr](reference/other-types.md#whenever.DateDeltaUnitStr)) → [float](https://docs.python.org/3/library/functions.html#float)
#### since(b: [Date](reference/date.md#whenever.Date), , , in_units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DateDeltaUnitStr')], round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = 'trunc', round_increment: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta)
Calculate the difference between this date and another date.
The difference is calculated in terms of the chosen calendar unit
or units.
```pycon
>>> d = Date(2023, 4, 15)
>>> d.since(Date("2020-01-01"), in_units=["years", "months"])
ItemizedDateDelta("P3y3m")
```
```pycon
>>> d.since(Date("2020-01-01"), total="weeks")
170.0
```
* **Parameters:**
* **other** – The date to calculate the difference since.
* **total** –
If specified, the difference is returned as a float in terms
of this single unit. Cannot be combined with `in_units`.
The fractional part is based on the number of days in the
surrounding calendar period — not a fixed conversion factor.
For example, 6 months from January 1 spans 181 days of a
365-day year, giving approximately 0.496 years, not 0.5.
* **in_units** – If specified, the difference is calculated in terms of these units,
in decreasing order of size. Cannot be combined with `total`.
* **round_mode** – The rounding mode to apply to the smallest specified unit.
Only valid with `in_units`.
* **round_increment** – The increment to round to for the smallest specified unit.
Only valid with `in_units`.
* **Returns:**
If `in_units` is specified, the difference is returned
as an [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta),
If `total` is specified, as a float number of the specified unit.
* **Return type:**
[ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) | [float](https://docs.python.org/3/library/functions.html#float)
#### start_of(unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['year', 'month', 'week_mon', 'week_sun'],) → [Date](reference/date.md#whenever.Date)
The start of the given calendar unit
```pycon
>>> Date(2024, 8, 15).start_of("year")
Date("2024-01-01")
>>> Date(2024, 8, 15).start_of("month")
Date("2024-08-01")
>>> Date(2024, 8, 15).start_of("week_mon")
Date("2024-08-12")
```
#### subtract(delta: [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) | [DateDelta](reference/deprecated.md#whenever.DateDelta),) → [Date](reference/date.md#whenever.Date)
#### subtract(, years: [int](https://docs.python.org/3/library/functions.html#int) = ..., months: [int](https://docs.python.org/3/library/functions.html#int) = ..., weeks: [int](https://docs.python.org/3/library/functions.html#int) = ..., days: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [Date](reference/date.md#whenever.Date)
Subtract components from a date.
See [the docs on arithmetic](guide/arithmetic.md#arithmetic) for more information.
```pycon
>>> 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")
```
#### to_stdlib() → [date](https://docs.python.org/3/library/datetime.html#datetime.date)
Convert to a standard library [`date`](https://docs.python.org/3/library/datetime.html#datetime.date)
#### until(b: [Date](reference/date.md#whenever.Date), , , total: [DateDeltaUnitStr](reference/other-types.md#whenever.DateDeltaUnitStr)) → [float](https://docs.python.org/3/library/functions.html#float)
#### until(b: [Date](reference/date.md#whenever.Date), , , in_units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DateDeltaUnitStr')], round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = 'trunc', round_increment: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta)
Companion to [`since()`](reference/date.md#whenever.Date.since) that calculates the difference until another date.
See [`since()`](reference/date.md#whenever.Date.since) for more information.
#### year_month() → [YearMonth](reference/yearmonth.md#whenever.YearMonth)
The year and month (without a day component)
```pycon
>>> Date(2021, 1, 2).year_month()
YearMonth("2021-01")
```
#### MAX *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[Date](reference/date.md#whenever.Date)]* *= Date("9999-12-31")*
The maximum possible date
#### MIN *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[Date](reference/date.md#whenever.Date)]* *= Date("0001-01-01")*
The minimum possible date
#### *property* day *: [int](https://docs.python.org/3/library/functions.html#int)*
The day component of the date
```pycon
>>> Date(2021, 1, 2).day
2
```
#### *property* month *: [int](https://docs.python.org/3/library/functions.html#int)*
The month component of the date
```pycon
>>> Date(2021, 1, 2).month
1
```
#### *property* year *: [int](https://docs.python.org/3/library/functions.html#int)*
The year component of the date
```pycon
>>> Date(2021, 1, 2).year
2021
```
# reference/datetime.md
# Main types
The `whenever` library provides four main date-time types, each
with its own purpose and behavior:
* [`Instant`](reference/instant.md)
* [`ZonedDateTime`](reference/zoned_datetime.md)
* [`OffsetDateTime`](reference/offset_datetime.md)
* [`PlainDateTime`](reference/plain_datetime.md)
The available methods differ between these types based on whether they
represent [exact time or local time](fundamentals/exact-vs-local.md#exact-vs-local):
| type | represents exact time? | represents local time? |
|-----------------------------------------------------------------------------------------|--------------------------|--------------------------|
| [`Instant`](reference/instant.md#whenever.Instant) | ✅ | ❌ |
| [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) | ✅ | ✅ |
| [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) | ✅ | ✅ |
| [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) | ❌ | ✅ |
## Exact time methods
The exact time classes ([`Instant`](reference/instant.md#whenever.Instant), [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime),
and [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime)) share several methods for working with
exact points in time:
| | [`Instant`](reference/instant.md#whenever.Instant) | [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) | [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) |
|-----------------------------------------|-------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| `now()` | [`🔗`](reference/instant.md#whenever.Instant.now) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.now) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.now) |
| | | | |
| `timestamp()` [1](#id5) | [`🔗`](reference/instant.md#whenever.Instant.timestamp) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.timestamp) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.timestamp) |
| `from_timestamp()` [2](#id6) | [`🔗`](reference/instant.md#whenever.Instant.from_timestamp) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.from_timestamp) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.from_timestamp) |
| | | | |
| `to_fixed_offset()` | [`🔗`](reference/instant.md#whenever.Instant.to_fixed_offset) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.to_fixed_offset) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.to_fixed_offset) |
| `to_tz()` | [`🔗`](reference/instant.md#whenever.Instant.to_tz) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.to_tz) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.to_tz) |
| `to_system_tz()` | [`🔗`](reference/instant.md#whenever.Instant.to_system_tz) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.to_system_tz) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.to_system_tz) |
| | | | |
| `x > other_exact` [3](#id7) | [`🔗`](reference/instant.md#whenever.Instant.__gt__) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.__gt__) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.__gt__) |
| `x - other_exact` | [`🔗`](reference/instant.md#whenever.Instant.__sub__) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.__sub__) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.__sub__) |
| `x == other_exact` | [`🔗`](reference/instant.md#whenever.Instant.__eq__) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.__eq__) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.__eq__) |
| `exact_eq()` | [`🔗`](reference/instant.md#whenever.Instant.exact_eq) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.exact_eq) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.exact_eq) |
| | | | |
| `x + TimeDelta` | [`🔗`](reference/instant.md#whenever.Instant.__add__) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.__add__) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.__add__) |
## Local time methods
The local time classes ([`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime), [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime),
and [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime)) share several methods for working with
local date and time values:
| | [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) | [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) | [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) |
|----------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `year`, `month`, etc. | [`🔗`](reference/plain_datetime.md#whenever.PlainDateTime.year) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.year) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.year) |
| `hour`, `minute`, etc. | [`🔗`](reference/plain_datetime.md#whenever.PlainDateTime.hour) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.hour) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.hour) |
| `date()` | [`🔗`](reference/plain_datetime.md#whenever.PlainDateTime.date) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.date) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.date) |
| `time()` | [`🔗`](reference/plain_datetime.md#whenever.PlainDateTime.time) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.time) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.time) |
| | | | |
| `replace()` [4](#id8) | [`🔗`](reference/plain_datetime.md#whenever.PlainDateTime.replace) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.replace) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.replace) |
| `add()`, `subtract()` | [`🔗`](reference/plain_datetime.md#whenever.PlainDateTime.add), [`🔗`](reference/plain_datetime.md#whenever.PlainDateTime.subtract) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.add), [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.subtract) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.add), [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.subtract) |
| `since()`, `until()` | [`🔗`](reference/plain_datetime.md#whenever.PlainDateTime.since), [`🔗`](reference/plain_datetime.md#whenever.PlainDateTime.until) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.since), [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.until) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.since), [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.until) |
| `round()` | [`🔗`](reference/plain_datetime.md#whenever.PlainDateTime.round) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.round) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.round) |
| | | | |
| `start_of()`, `end_of()` | [`🔗`](reference/plain_datetime.md#whenever.PlainDateTime.start_of), [`🔗`](reference/plain_datetime.md#whenever.PlainDateTime.end_of) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.start_of), [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.end_of) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.start_of), [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.end_of) |
| | | | |
| `day_of_year()` | [`🔗`](reference/plain_datetime.md#whenever.PlainDateTime.day_of_year) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.day_of_year) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.day_of_year) |
| `days_in_month()` | [`🔗`](reference/plain_datetime.md#whenever.PlainDateTime.days_in_month) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.days_in_month) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.days_in_month) |
| `days_in_year()` | [`🔗`](reference/plain_datetime.md#whenever.PlainDateTime.days_in_year) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.days_in_year) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.days_in_year) |
| `in_leap_year()` | [`🔗`](reference/plain_datetime.md#whenever.PlainDateTime.in_leap_year) | [`🔗`](reference/zoned_datetime.md#whenever.ZonedDateTime.in_leap_year) | [`🔗`](reference/offset_datetime.md#whenever.OffsetDateTime.in_leap_year) |
#### NOTE
Although [`Instant`](reference/instant.md#whenever.Instant)’s debug representation is in
UTC, it does not have local time methods.
See the [FAQ](faq.md#faq-instant-no-local) for more details.
## Other methods
Several other methods are unique to one or more classes:
| [`Instant`](reference/instant.md#whenever.Instant) | [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) | [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) | [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) |
|------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`MIN`](reference/instant.md#whenever.Instant.MIN), [`MAX`](reference/instant.md#whenever.Instant.MAX) | | | [`MIN`](reference/plain_datetime.md#whenever.PlainDateTime.MIN), [`MAX`](reference/plain_datetime.md#whenever.PlainDateTime.MAX) |
| [`from_utc()`](reference/instant.md#whenever.Instant.from_utc) | | | |
| [`format_rfc2822`](reference/instant.md#whenever.Instant.format_rfc2822) | | [`format_rfc2822()`](reference/offset_datetime.md#whenever.OffsetDateTime.format_rfc2822) | |
| | [`to_instant()`](reference/zoned_datetime.md#whenever.ZonedDateTime.to_instant) | [`to_instant()`](reference/offset_datetime.md#whenever.OffsetDateTime.to_instant) | |
| | [`to_plain()`](reference/zoned_datetime.md#whenever.ZonedDateTime.to_plain) | [`to_plain()`](reference/offset_datetime.md#whenever.OffsetDateTime.to_plain) | |
| | [`offset`](reference/zoned_datetime.md#whenever.ZonedDateTime.offset) | [`offset`](reference/offset_datetime.md#whenever.OffsetDateTime.offset) | |
| | | | [`x == other_plain`](reference/plain_datetime.md#whenever.PlainDateTime.__eq__) |
| | | | [`x > other_plain`](reference/plain_datetime.md#whenever.PlainDateTime.__gt__) |
| | | | [`x - other_plain`](reference/plain_datetime.md#whenever.PlainDateTime.__sub__) |
| | | | [`assume_utc()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_utc) |
| | | [`assume_tz()`](reference/offset_datetime.md#whenever.OffsetDateTime.assume_tz) | [`assume_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_tz) |
| | | | [`assume_system_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_system_tz) |
| | | | [`assume_fixed_offset()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_fixed_offset) |
| | | [`parse_strptime()`](reference/offset_datetime.md#whenever.OffsetDateTime.parse_strptime) *(deprecated)* | [`parse_strptime()`](reference/plain_datetime.md#whenever.PlainDateTime.parse_strptime) *(deprecated)* |
| | [`tz`](reference/zoned_datetime.md#whenever.ZonedDateTime.tz) | | |
| | [`now_in_system_tz()`](reference/zoned_datetime.md#whenever.ZonedDateTime.now_in_system_tz) | | |
| | [`is_ambiguous()`](reference/zoned_datetime.md#whenever.ZonedDateTime.is_ambiguous) | | |
| | [`dst_offset()`](reference/zoned_datetime.md#whenever.ZonedDateTime.dst_offset) | | |
| | [`tz_abbrev()`](reference/zoned_datetime.md#whenever.ZonedDateTime.tz_abbrev) | | |
| | [`day_length()`](reference/zoned_datetime.md#whenever.ZonedDateTime.day_length) | | |
| | [`next_transition()`](reference/zoned_datetime.md#whenever.ZonedDateTime.next_transition) | | |
| | [`prev_transition()`](reference/zoned_datetime.md#whenever.ZonedDateTime.prev_transition) | | |
| | [`start_of_day()`](reference/zoned_datetime.md#whenever.ZonedDateTime.start_of_day) *(deprecated)* | | |
---
* **[1]** `timestamp_millis()` and `timestamp_nanos()` methods are also available for millisecond and nanosecond precision.
* **[2]** `from_timestamp_millis()` and `from_timestamp_nanos()` methods are also available for millisecond and nanosecond precision.
* **[3]** The other comparison operators `<=`, `<`, and `>=` are also supported.
* **[4]** `replace_date()` and `replace_time()` are also available for replacing only the date or time component.
# reference/deltas.md
# Delta types
#### TIP
For a quick introduction to adding and subtracting time,
see [Arithmetic](guide/arithmetic.md#arithmetic). This page goes into more detail on
working with durations as standalone objects.
As we’ve seen [earlier](guide/arithmetic.md#add-subtract-time), you can add and subtract
time units from datetimes:
```python
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 key concept is that there are **three different delta types**,
each suited for different use cases:
- Use [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) if you’re working with [`Instant`](reference/instant.md#whenever.Instant)
or exact time units (hours, minutes, seconds). Similar to [`datetime.timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta).
- Use [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) if you’re working [`Date`](reference/date.md#whenever.Date) or
only with calendar units (years, months, days).
- Use [`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta) if you need to work with *both* with calendar units
(years, months, days) and exact time units (hours, minutes, seconds).
#### NOTE
[`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta) and [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) were introduced in version 0.10,
and replace the (now deprecated) [`DateTimeDelta`](reference/deprecated.md#whenever.DateTimeDelta) and [`DateDelta`](reference/deprecated.md#whenever.DateDelta) classes.
Here is a summary of the three delta types provided,
and their key differences. Click on the features to learn more about them.
## Overview
| Feature | [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) | [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) | [`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta) |
|----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [Supported units](reference/deltas.md#delta-units) | exact units | calendar units | exact *and* calendar units |
| [Normalized](reference/deltas.md#delta-norm) | yes | no | no |
| [Equality](reference/deltas.md#delta-eq) | [`normalized`](reference/time_delta.md#whenever.TimeDelta.__eq__) | [`itemwise`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta.__eq__) | [`itemwise`](reference/itemized_delta.md#whenever.ItemizedDelta.__eq__) |
| [Convert to units](reference/deltas.md#delta-in-units) | [`in_units()`](reference/time_delta.md#whenever.TimeDelta.in_units) | [`in_units()`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta.in_units) [1](#id7) | [`in_units()`](reference/itemized_delta.md#whenever.ItemizedDelta.in_units) [1](#id7) |
| [Summing into one unit](reference/deltas.md#delta-total) | [`total()`](reference/time_delta.md#whenever.TimeDelta.total) | [`total()`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta.total) [1](#id7) | [`total()`](reference/itemized_delta.md#whenever.ItemizedDelta.total) [1](#id7) |
| [Comparison](reference/deltas.md#delta-cmp) | [`>`](reference/time_delta.md#whenever.TimeDelta.__gt__) , [`<`](reference/time_delta.md#whenever.TimeDelta.__lt__) , [`>=`](reference/time_delta.md#whenever.TimeDelta.__ge__) , [`<=`](reference/time_delta.md#whenever.TimeDelta.__le__) | n/a | n/a |
| [Addition/subtraction](reference/deltas.md#delta-add-sub) | [`add()`](reference/time_delta.md#whenever.TimeDelta.add) / [`subtract()`](reference/time_delta.md#whenever.TimeDelta.subtract) | [`add()`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta.add) / [`subtract()`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta.subtract) | [`add()`](reference/itemized_delta.md#whenever.ItemizedDelta.add) / [`subtract()`](reference/itemized_delta.md#whenever.ItemizedDelta.subtract) |
| [Operators](reference/deltas.md#delta-operators) | [`+`](reference/time_delta.md#whenever.TimeDelta.__add__) , [`-`](reference/time_delta.md#whenever.TimeDelta.__sub__) , [`*`](reference/time_delta.md#whenever.TimeDelta.__mul__) , [`/`](reference/time_delta.md#whenever.TimeDelta.__truediv__) , [`//`](reference/time_delta.md#whenever.TimeDelta.__floordiv__) , [`%`](reference/time_delta.md#whenever.TimeDelta.__mod__) | `+`, `-` | `+`, `-` |
| [Rounding](reference/deltas.md#delta-rounding) | [`round()`](reference/time_delta.md#whenever.TimeDelta.round) | with [`in_units()`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta.in_units) | with [`in_units()`](reference/itemized_delta.md#whenever.ItemizedDelta.in_units) |
| Applies to… | [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) [`Instant`](reference/instant.md#whenever.Instant) | [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) [`Date`](reference/date.md#whenever.Date) | [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) |
| Similar to… | [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta) | [`Counter`](https://docs.python.org/3/library/collections.html#collections.Counter) | [`Counter`](https://docs.python.org/3/library/collections.html#collections.Counter) |
## Exact and calendar units
A key distinction when working with durations
is between exact time units and calendar units.
See [the fundamentals](fundamentals/arithmetic.md#arithmetic2) for an in-depth explanation.
In short:
- **Exact units** (hours, minutes, seconds) have a fixed duration.
- **Calendar units** (years, months, weeks, days) have a variable duration
depending on context (e.g. leap years, DST).
Depending on the units you need to work with, you should choose the appropriate delta type:
- [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) for exact time units
- [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) for calendar units
- [`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta) for a combination of the two
## Normalized or “itemized”
These delta classes also differ in how their components are stored.
“Itemized” deltas keep track of their individual components
(years, months, days, hours, minutes, seconds) separately, without normalizing them
into each other.
For example, an [`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta) of “1 hour and 90 minutes” will keep its components
as “1 hour” and “90 minutes”, without converting the 90 minutes into 1 hour and 30 minutes.
This is essential when working with calendar units,
and sometimes useful when working with exact time units.
```python
>>> d = ItemizedDelta(hours=1, minutes=90)
ItemizedDelta("PT1h90m")
```
You can imagine this working like a `dict` or [`Counter`](https://docs.python.org/3/library/collections.html#collections.Counter) of components,
where each unit is a key and its value is the corresponding amount:
```python3
>>> dict(d)
{'hours': 1, 'minutes': 90}
```
[`TimeDelta`](reference/time_delta.md#whenever.TimeDelta), on the other hand, normalizes all its components into each other.
So “1 hour and 90 minutes” becomes “2 hours and 30 minutes”.
This enables easier arithmetic and comparisons,
as their duration is always the same.
```python
>>> d = TimeDelta(hours=1, minutes=90)
TimeDelta("PT2h30m")
```
You can imagine this working like a big `int` of nanoseconds internally, which is then converted back into the appropriate units when needed:
```python
>>> d.total("minutes")
150.0
>>> d.total("nanoseconds")
9000000000000
```
## Equality
The difference between “itemized” and “normalized” is reflected in equality checks.
Itemized deltas are considered equal
only if all their individual components are the same:
```python
>>> ItemizedDelta(hours=1, minutes=90) == ItemizedDelta(hours=2, minutes=30)
False # items are not the same
```
Normalized deltas are considered equal
if their total duration is the same, regardless of how their components are represented:
```python
>>> TimeDelta(hours=1, minutes=90) == TimeDelta(hours=2, minutes=30)
True # normalized durations are the same
```
## Sign
All delta types carry a single sign that applies to every component
uniformly—there are no mixed-sign deltas.
```python
>>> ItemizedDelta(months=-3, days=-10, hours=-5)
ItemizedDelta("-P3m10dT5h")
>>> -ItemizedDateDelta(years=1, months=6)
ItemizedDateDelta("-P1y6m")
```
Negating a delta flips the sign of all components at once:
```python
>>> d = ItemizedDelta(hours=2, minutes=30)
>>> -d
ItemizedDelta("-PT2h30m")
```
[`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) also has a single sign, but may be constructed
with mixed-sign components, as they will be normalized into a single sign automatically:
```python
>>> d = TimeDelta(hours=1, minutes=-15)
>>> d
TimeDelta("PT45m")
```
## Convert into specific units
All delta types can be converted into specific units using
their `in_units()` method.
This is sometimes called “balancing”—redistributing the value
across the requested units:
```python
>>> delta = TimeDelta(hours=3, minutes=2, seconds=5)
>>> delta.in_units(["minutes", "seconds"])
ItemizedDelta("PT182m5s")
>>> # deltas can also be unpacked directly:
>>> hours, minutes = delta.in_units(["hours", "minutes"]).values()
(3, 2)
```
For example, 150 minutes balanced into hours and minutes:
```python
>>> TimeDelta(minutes=150).in_units(["hours", "minutes"]).values()
(2, 30)
```
#### TIP
If you need the difference between two datetimes in specific units,
use [`since()`](reference/zoned_datetime.md#whenever.ZonedDateTime.since) / [`until()`](reference/zoned_datetime.md#whenever.ZonedDateTime.until)
instead of computing a delta and converting it.
See [Arithmetic](guide/arithmetic.md#arithmetic).
If you’d like to convert into a single unit instead, see the next section.
## Summing into a single unit
All delta types can also be summed into a single unit using
their `total()` method, which returns a `float`.
```python
>>> d = TimeDelta(hours=2, minutes=30, seconds=6)
>>> d.total("minutes")
150.1
```
When the total duration is requested in `"nanoseconds"` (the smallest supported unit),
`total()` returns an `int` instead of a `float` to avoid precision issues.
#### NOTE
For [`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta) and [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta),
both `in_units()` and `total()` require a `relative_to` parameter to
resolve calendar units.
This is because calendar units have variable lengths—`1 month` is
28, 29, 30, or 31 days depending on the starting date—so the conversion
can only be performed with a concrete reference point.
See the individual class reference pages for details.
## Comparison
Only [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) supports comparison operators
(such as `>`, `<`, `>=`, and `<=`),
as these operations only make sense when exclusively working with exact time units:
```python
>>> TimeDelta(minutes=90) > TimeDelta(hours=1)
True
```
[`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) and [`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta) do not support comparison operators,
as they may contain calendar units, which have variable durations depending on context.
For example, it’s not possible to say whether “1 month” is greater than “30 days” in general.
```python
>>> a = ItemizedDateDelta(months=1)
>>> b = ItemizedDateDelta(days=30)
>>> a > b # TypeError
```
One way to compare itemized deltas is to convert them into one specific unit first,
using their `total()` method and a relative date or datetime context:
```python
>>> date = Date(2023, 1, 1)
>>> a.total("days", relative_to=date) > b.total("days", relative_to=date)
True
```
## Addition and subtraction
All three delta types support addition and subtraction
using the [`add()`](reference/itemized_delta.md#whenever.ItemizedDelta.add) and [`subtract()`](reference/itemized_delta.md#whenever.ItemizedDelta.subtract) methods.
These methods return a new delta representing the sum or difference
of the two deltas:
```python
>>> TimeDelta(hours=2, minutes=30).add(hours=1)
TimeDelta("PT3h30m")
```
“Itemized” delta composition can use a relative date or datetime context
to resolve calendar units when adding or subtracting.
For example, the calendar-aware composition of “1 month” and “30 days”
depends on the starting date:
```python
>>> one_month = ItemizedDateDelta(months=1)
>>> one_month.add(days=30, relative_to=Date(2023, 1, 1))
ItemizedDateDelta("P2m2d")
>>> one_month.add(days=30, relative_to=Date(2023, 2, 28))
ItemizedDateDelta("P1m30d")
```
Without a `relative_to` reference, itemized-delta composition is field-wise.
That preserves the literal fields, but it can change the meaning of later
application to a datetime because calendar units do not reliably compose.
The operation emits
[`CalendarUnitCompositionWarning`](reference/exceptions.md#whenever.CalendarUnitCompositionWarning) when either operand contains
nonzero calendar units, unless you pass `cal_unit_composition_ok=True`.
Exact-only composition does not warn.
## Operators
Multiplication and division are only supported for [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta), because
these operations only make sense for exact time units.
```python
>>> delta = TimeDelta(hours=2, minutes=30)
>>> delta * 2
TimeDelta("PT5h")
>>> delta / 2
TimeDelta("PT1h15m")
```
Itemized deltas also support `+` and `-`, but those operators perform
field-wise composition and emit
[`CalendarUnitCompositionWarning`](reference/exceptions.md#whenever.CalendarUnitCompositionWarning) when either operand contains
nonzero calendar units. Exact-only composition does not warn.
Use the method forms if you want to pass `cal_unit_composition_ok=True`
or if you need calendar-aware composition via `relative_to`.
Dates and datetimes support applying an itemized delta with `+` and `-`.
Addition is also commutative in spelling, so both `datetime + delta` and
`delta + datetime` are supported. These operations use the date or datetime
as their reference and do not emit `CalendarUnitCompositionWarning`.
As with the equivalent `add()` and `subtract()` methods, calendar clamping
means that adding and then subtracting the same delta is not always reversible.
## Rounding
Only [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) has a [`round()`](reference/time_delta.md#whenever.TimeDelta.round) method for rounding to a specific unit:
```python
>>> delta = TimeDelta(hours=2, minutes=30, seconds=3)
>>> delta.round("hour")
TimeDelta("PT3h")
```
Rounding an itemized delta can only be done by also normalizing it,
using the [`in_units()`](reference/itemized_delta.md#whenever.ItemizedDelta.in_units) method:
```python
>>> delta = ItemizedDelta(days=7, hours=2, minutes=84)
>>> delta.in_units(
... ["days", "hours"],
... relative_to=ZonedDateTime(2020, 1, 1, tz="UTC"),
... round_mode="ceil",
... round_increment=4
... )
ItemizedDelta("P7dT4h")
```
See [Rounding](guide/rounding.md#rounding) for more information on rounding modes and increments.
## ISO 8601 format
The ISO 8601 standard defines formats for specifying durations,
the [most common](https://en.wikipedia.org/wiki/ISO_8601#Durations) being:
```text
±P nY nM nD T nH nM nS (spaces added for clarity)
```
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.
- At least one component must be present (it may be zero).
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:
| Delta Type | Format Method | Parse Method |
|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------|
| [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) | [`format_iso()`](reference/time_delta.md#whenever.TimeDelta.format_iso) | [`parse_iso()`](reference/time_delta.md#whenever.TimeDelta.parse_iso) |
| [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) | [`format_iso()`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta.format_iso) | [`parse_iso()`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta.parse_iso) |
| [`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta) | [`format_iso()`](reference/itemized_delta.md#whenever.ItemizedDelta.format_iso) | [`parse_iso()`](reference/itemized_delta.md#whenever.ItemizedDelta.parse_iso) |
```python
>>> TimeDelta(hours=3).format_iso()
'PT3H'
>>> ItemizedDelta(years=-1, months=-3, seconds=-15).format_iso()
'-P1Y3MT15S'
>>> ItemizedDateDelta.parse_iso('-P2M')
ItemizedDateDelta("-P2m")
>>> ItemizedDelta.parse_iso('P3YT90M')
ItemizedDelta("P3yT90m")
```
## Equivalents in other languages
The three delta types in `whenever` are similar to those in other languages:
| Library | [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) | [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) | [`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta) |
|------------------|---------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
| NodaTime (C#) | `Duration` | [2](#id8) | `Period` |
| java.time (Java) | `Duration` | `Period` | `PeriodDuration` [3](#id9) |
| Jiff (Rust) | `SignedDuration` | | `Span` |
| Temporal (JS) | | | `Duration` |
---
* **[1]** These operations require a relative date or datetime context to resolve calendar units.
* **[2]** The author of NodaTime has been tempted to [include it](https://github.com/nodatime/nodatime/issues/1435#issuecomment-547855819) though
* **[3]** Part of the [ThreeTen-Extra](https://www.threeten.org/threeten-extra/) library by the same author.
# reference/deprecated.md
# Deprecated components
### whenever.years(i: [int](https://docs.python.org/3/library/functions.html#int),) → [DateDelta](reference/deprecated.md#whenever.DateDelta)
Create a [`DateDelta`](reference/deprecated.md#whenever.DateDelta) with the given number of years.
`years(1) == DateDelta(years=1)`
#### Deprecated
Deprecated since version 0.10.0: Use [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) instead
### whenever.months(i: [int](https://docs.python.org/3/library/functions.html#int),) → [DateDelta](reference/deprecated.md#whenever.DateDelta)
Create a [`DateDelta`](reference/deprecated.md#whenever.DateDelta) with the given number of months.
`months(1) == DateDelta(months=1)`
#### Deprecated
Deprecated since version 0.10.0: Use [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) instead
### whenever.weeks(i: [int](https://docs.python.org/3/library/functions.html#int),) → [DateDelta](reference/deprecated.md#whenever.DateDelta)
Create a [`DateDelta`](reference/deprecated.md#whenever.DateDelta) with the given number of weeks.
`weeks(1) == DateDelta(weeks=1)`
#### Deprecated
Deprecated since version 0.10.0: Use [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) instead
### whenever.days(i: [int](https://docs.python.org/3/library/functions.html#int),) → [DateDelta](reference/deprecated.md#whenever.DateDelta)
Create a [`DateDelta`](reference/deprecated.md#whenever.DateDelta) with the given number of days.
`days(1) == DateDelta(days=1)`
#### Deprecated
Deprecated since version 0.10.0: Use [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) instead
### *exception* whenever.ImplicitlyIgnoringDST
Raised when an operation would silently ignore DST transitions.
#### Deprecated
Deprecated since version 0.10.0: This exception is deprecated and will be removed in a future version.
### *class* whenever.DateDelta(iso_string: [str](https://docs.python.org/3/library/stdtypes.html#str),)
### *class* whenever.DateDelta(, years: [int](https://docs.python.org/3/library/functions.html#int) = ..., months: [int](https://docs.python.org/3/library/functions.html#int) = ..., weeks: [int](https://docs.python.org/3/library/functions.html#int) = ..., days: [int](https://docs.python.org/3/library/functions.html#int) = ...)
A duration of time consisting of calendar units
(years, months, weeks, and days).
#### Deprecated
Deprecated since version 0.10.0: Use [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) instead.
`DateDelta` normalizes its inputs (e.g. 14 months becomes
1 year and 2 months), losing the original fields.
`ItemizedDateDelta` preserves the exact fields it was created with.
#### *classmethod* parse_iso(s: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [DateDelta](reference/deprecated.md#whenever.DateDelta)
Parse the *popular interpretation* of the ISO 8601 duration format.
Does not parse all possible ISO 8601 durations.
See [here](reference/deltas.md#iso8601-durations) for more information.
Inverse of [`format_iso()`](reference/deprecated.md#whenever.DateDelta.format_iso)
```pycon
>>> 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.
#### \_\_abs_\_() → [DateDelta](reference/deprecated.md#whenever.DateDelta)
If the contents are negative, return the positive version
```pycon
>>> p = DateDelta(months=-2, days=-3)
>>> abs(p)
DateDelta("P2m3d")
```
#### \_\_add_\_(other: [DateDelta](reference/deprecated.md#whenever.DateDelta)) → [DateDelta](reference/deprecated.md#whenever.DateDelta)
#### \_\_add_\_(other: [TimeDelta](reference/time_delta.md#whenever.TimeDelta)) → [DateTimeDelta](reference/deprecated.md#whenever.DateTimeDelta)
Add the fields of another delta to this one
```pycon
>>> p = DateDelta(weeks=2, months=1)
>>> p + DateDelta(weeks=1, days=4)
DateDelta("P1m25d")
```
#### \_\_bool_\_() → [bool](https://docs.python.org/3/library/functions.html#bool)
True if any contains any non-zero data
```pycon
>>> bool(DateDelta())
False
>>> bool(DateDelta(days=-1))
True
```
#### \_\_eq_\_(other: [object](https://docs.python.org/3/library/functions.html#object)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare for equality, normalized to months and days.
a == b is equivalent to a.in_months_days() == b.in_months_days()
```pycon
>>> 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](https://docs.python.org/3/library/functions.html#int)) → [DateDelta](reference/deprecated.md#whenever.DateDelta)
Multiply the contents by a round number
```pycon
>>> p = DateDelta(years=1, weeks=2)
>>> p * 2
DateDelta("P2y28d")
```
#### \_\_neg_\_() → [DateDelta](reference/deprecated.md#whenever.DateDelta)
Negate the contents
```pycon
>>> p = DateDelta(weeks=2, days=3)
>>> -p
DateDelta(-P17d)
```
#### \_\_sub_\_(other: [DateDelta](reference/deprecated.md#whenever.DateDelta)) → [DateDelta](reference/deprecated.md#whenever.DateDelta)
#### \_\_sub_\_(other: [TimeDelta](reference/time_delta.md#whenever.TimeDelta)) → [DateTimeDelta](reference/deprecated.md#whenever.DateTimeDelta)
Subtract the fields of another delta from this one
```pycon
>>> p = DateDelta(weeks=2, days=3)
>>> p - DateDelta(days=2)
DateDelta("P15d")
```
#### format_iso() → [str](https://docs.python.org/3/library/stdtypes.html#str)
Format as the *popular interpretation* of the ISO 8601 duration format.
May not strictly adhere to (all versions of) the standard.
See [here](reference/deltas.md#iso8601-durations) for more information.
Inverse of [`parse_iso()`](reference/deprecated.md#whenever.DateDelta.parse_iso).
```pycon
>>> p = DateDelta(years=1, months=2, weeks=3, days=11)
>>> p.format_iso()
'P1Y2M3W11D'
>>> DateDelta().format_iso()
'P0D'
```
The format looks like this:
```text
P(nY)(nM)(nD)
```
For example:
```text
P1D
P2M
P1Y2M3W4D
```
#### in_months_days() → [tuple](https://docs.python.org/3/library/stdtypes.html#tuple)[[int](https://docs.python.org/3/library/functions.html#int), [int](https://docs.python.org/3/library/functions.html#int)]
Convert to a tuple of months and days.
```pycon
>>> p = DateDelta(months=25, days=9)
>>> p.in_months_days()
(25, 9)
>>> DateDelta(months=-13, weeks=-5)
(-13, -35)
```
#### in_years_months_days() → [tuple](https://docs.python.org/3/library/stdtypes.html#tuple)[[int](https://docs.python.org/3/library/functions.html#int), [int](https://docs.python.org/3/library/functions.html#int), [int](https://docs.python.org/3/library/functions.html#int)]
Convert to a tuple of years, months, and days.
```pycon
>>> p = DateDelta(years=1, months=2, days=11)
>>> p.in_years_months_days()
(1, 2, 11)
```
#### ZERO *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[DateDelta](reference/deprecated.md#whenever.DateDelta)]* *= DateDelta("P0d")*
A delta of zero
### *class* whenever.DateTimeDelta(iso_string: [str](https://docs.python.org/3/library/stdtypes.html#str),)
### *class* whenever.DateTimeDelta(, years: [int](https://docs.python.org/3/library/functions.html#int) = ..., months: [int](https://docs.python.org/3/library/functions.html#int) = ..., weeks: [int](https://docs.python.org/3/library/functions.html#int) = ..., days: [int](https://docs.python.org/3/library/functions.html#int) = ..., hours: [float](https://docs.python.org/3/library/functions.html#float) = ..., minutes: [float](https://docs.python.org/3/library/functions.html#float) = ..., seconds: [float](https://docs.python.org/3/library/functions.html#float) = ..., milliseconds: [float](https://docs.python.org/3/library/functions.html#float) = ..., microseconds: [float](https://docs.python.org/3/library/functions.html#float) = ..., nanoseconds: [int](https://docs.python.org/3/library/functions.html#int) = ...)
A duration with both a date and time component.
#### Deprecated
Deprecated since version 0.10.0: Use [`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta) instead.
`DateTimeDelta` normalizes its inputs separately for the date
and time parts, losing the original fields.
`ItemizedDelta` preserves the exact fields it was created with.
#### *classmethod* parse_iso(s: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [DateTimeDelta](reference/deprecated.md#whenever.DateTimeDelta)
Parse the *popular interpretation* of the ISO 8601 duration format.
Does not parse all possible ISO 8601 durations.
See [here](reference/deltas.md#iso8601-durations) for more information.
```text
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()`](reference/deprecated.md#whenever.DateTimeDelta.format_iso)
```pycon
>>> DateTimeDelta.parse_iso("-P1W11DT4H")
DateTimeDelta(-P1w11dT4h)
```
#### \_\_abs_\_() → [DateTimeDelta](reference/deprecated.md#whenever.DateTimeDelta)
The absolute value of the delta
```pycon
>>> d = DateTimeDelta(weeks=1, days=-11, hours=4)
>>> abs(d)
DateTimeDelta("P1w11dT4h")
```
#### \_\_add_\_(other: [DateTimeDelta](reference/deprecated.md#whenever.DateTimeDelta) | [DateDelta](reference/deprecated.md#whenever.DateDelta) | [TimeDelta](reference/time_delta.md#whenever.TimeDelta)) → [DateTimeDelta](reference/deprecated.md#whenever.DateTimeDelta)
Add two deltas together
```pycon
>>> d = DateTimeDelta(weeks=1, days=11, hours=4)
>>> d + DateTimeDelta(months=2, days=3, minutes=90)
DateTimeDelta("P1m1w14dT5h30m")
```
#### \_\_bool_\_() → [bool](https://docs.python.org/3/library/functions.html#bool)
True if any field is non-zero
```pycon
>>> bool(DateTimeDelta())
False
>>> bool(DateTimeDelta(minutes=1))
True
```
#### \_\_eq_\_(other: [object](https://docs.python.org/3/library/functions.html#object)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare for equality
```pycon
>>> 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](https://docs.python.org/3/library/functions.html#int)) → [DateTimeDelta](reference/deprecated.md#whenever.DateTimeDelta)
Multiply by a number
```pycon
>>> d = DateTimeDelta(weeks=1, days=11, hours=4)
>>> d * 2
DateTimeDelta("P2w22dT8h")
```
#### \_\_neg_\_() → [DateTimeDelta](reference/deprecated.md#whenever.DateTimeDelta)
Negate the delta
```pycon
>>> d = DateTimeDelta(days=11, hours=4)
>>> -d
DateTimeDelta(-P11dT4h)
```
#### \_\_sub_\_(other: [DateTimeDelta](reference/deprecated.md#whenever.DateTimeDelta) | [TimeDelta](reference/time_delta.md#whenever.TimeDelta) | [DateDelta](reference/deprecated.md#whenever.DateDelta)) → [DateTimeDelta](reference/deprecated.md#whenever.DateTimeDelta)
Subtract two deltas
```pycon
>>> d = DateTimeDelta(weeks=1, days=11, hours=4)
>>> d - DateTimeDelta(months=2, days=3, minutes=90)
DateTimeDelta(-P2m1w8dT2h30m)
```
#### date_part() → [DateDelta](reference/deprecated.md#whenever.DateDelta)
The date part of the delta
#### Deprecated
Deprecated since version 0.10.0.
#### format_iso() → [str](https://docs.python.org/3/library/stdtypes.html#str)
Format as the *popular interpretation* of the ISO 8601 duration format.
May not strictly adhere to (all versions of) the standard.
See [here](reference/deltas.md#iso8601-durations) for more information.
Inverse of [`parse_iso()`](reference/deprecated.md#whenever.DateTimeDelta.parse_iso).
The format is:
```text
P(nY)(nM)(nD)T(nH)(nM)(nS)
```
```pycon
>>> d = DateTimeDelta(
... weeks=1,
... days=11,
... hours=4,
... milliseconds=12,
... )
>>> d.format_iso()
'P1W11DT4H0.012S'
```
#### in_months_days_secs_nanos() → [tuple](https://docs.python.org/3/library/stdtypes.html#tuple)[[int](https://docs.python.org/3/library/functions.html#int), [int](https://docs.python.org/3/library/functions.html#int), [int](https://docs.python.org/3/library/functions.html#int), [int](https://docs.python.org/3/library/functions.html#int)]
Convert to a tuple of (months, days, seconds, nanoseconds)
```pycon
>>> d = DateTimeDelta(weeks=1, days=11, hours=4, microseconds=2)
>>> d.in_months_days_secs_nanos()
(0, 18, 14_400, 2000)
```
#### time_part() → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
The time part of the delta
#### ZERO *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[DateTimeDelta](reference/deprecated.md#whenever.DateTimeDelta)]* *= DateTimeDelta("P0d")*
A delta of zero
# reference/exceptions.md
# Exceptions and warnings
## Warnings
### *exception* whenever.WheneverWarning
Bases: [`UserWarning`](https://docs.python.org/3/library/exceptions.html#UserWarning)
Base class for all warnings emitted by the `whenever` library.
This can be used with Python’s standard warning filters to suppress or
escalate all warnings emitted by `whenever`:
```python
import warnings, whenever
warnings.filterwarnings("error", category=whenever.WheneverWarning)
```
### *exception* whenever.NaiveArithmeticWarning
Bases: [`PotentialDstBugWarning`](reference/exceptions.md#whenever.PotentialDstBugWarning)
Raised when exact-time arithmetic is performed on a
[`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) without timezone context.
[`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) carries no timezone information, so it
can’t account for DST transitions. When you add or subtract exact time
units (hours, minutes, seconds) or measure the exact difference between
two [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) values, the computation treats every
hour as equal. If a timezone transition falls in the interval, the result
may be off by an hour or more.
### When it can occur
```python
from whenever import PlainDateTime
# On 2023-10-29, Amsterdam clocks fall back at 3:00 AM.
# PlainDateTime has no knowledge of this.
d = PlainDateTime(2023, 10, 29, 1, 30)
d.add(hours=2) # NaiveArithmeticWarning
# PlainDateTime("2023-10-29 03:30:00")
# ^^ only 1 real hour passed in Amsterdam (clocks went back)
# Also emitted for exact-unit differences:
d2 = PlainDateTime(2023, 10, 30, 1, 30)
d2 - d # NaiveArithmeticWarning
```
### How to fix it
Attach a timezone with [`assume_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_tz) first,
then perform arithmetic on the resulting [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime):
```python
d.assume_tz("Europe/Amsterdam").add(hours=2)
# ZonedDateTime("2023-10-29 02:30:00+01:00[Europe/Amsterdam]") ✓
```
To suppress when timezone context doesn’t apply (e.g. simulations,
clock times not tied to a real-world timezone, or when you know no
transitions occur in the interval), pass `naive_arithmetic_ok=True`
(or use Python’s standard warning filters):
```python
d.add(hours=2, naive_arithmetic_ok=True)
```
### *exception* whenever.CalendarUnitCompositionWarning
Bases: [`WheneverWarning`](reference/exceptions.md#whenever.WheneverWarning)
Warn when itemized deltas are composed field by field.
Itemized deltas preserve the exact fields they were created with:
`1 month` remains `1 month` rather than being normalized to days.
Composing two itemized deltas without a `relative_to` reference therefore
performs literal field-wise arithmetic, such as
`ItemizedDateDelta(months=1) + ItemizedDateDelta(months=1)` becoming
`ItemizedDateDelta(months=2)`.
This is often useful for display and ISO 8601 round-tripping, but it is
not the same as sequentially applying both deltas to a date or datetime.
Calendar units do not compose reliably: for example, adding one month to
January 31 may clamp to the end of February, so adding another month from
there can differ from adding two months to January 31 in one step.
The warning is only emitted when either operand contains a nonzero calendar
unit; exact-only composition does not warn.
To preserve calendar-aware semantics, pass `relative_to=...` and
`in_units=...` to [`add()`](reference/itemized_delta.md#whenever.ItemizedDelta.add) or
[`add()`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta.add). If field-wise composition is
intentional, pass `cal_unit_composition_ok=True` or use Python’s
standard warning filters.
### *exception* whenever.DaysAssumed24HoursWarning
Bases: [`PotentialDstBugWarning`](reference/exceptions.md#whenever.PotentialDstBugWarning)
Raised when days are treated as exactly 24 hours, which may be wrong
across a DST transition.
[`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) always represents exact time.
Constructing one with `days` or `weeks` kwargs converts those units
to nanoseconds using fixed 86400-second days. If you later add this delta
to a [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) on a day where clocks spring forward
or fall back, the local time of the result will be off by the transition
length (usually one hour).
### When it can occur
```python
from whenever import TimeDelta, ZonedDateTime
# TimeDelta(days=1) is exactly 86 400 seconds — no DST awareness.
delta = TimeDelta(days=1) # DaysAssumed24HoursWarning
# Adding it to a ZonedDateTime on a spring-forward day gives the
# wrong local time:
eve = ZonedDateTime(2025, 3, 30, 12, tz="Europe/Amsterdam")
eve + delta
# ZonedDateTime("2025-03-31 13:00:00+02:00[Europe/Amsterdam]")
# ^^ 13:00, not 12:00 — one hour lost to the DST transition
```
### How to fix it
Use calendar-based arithmetic directly on the datetime to preserve
local time across transitions:
```python
eve.add(days=1)
# ZonedDateTime("2025-03-31 12:00:00+02:00[Europe/Amsterdam]") ✓
```
To suppress when exact 24-hour arithmetic is genuinely intended, pass
`days_assumed_24h_ok=True` (or use Python’s standard warning filters):
```python
TimeDelta(days=1, days_assumed_24h_ok=True)
```
### *exception* whenever.StaleOffsetWarning
Bases: [`PotentialDstBugWarning`](reference/exceptions.md#whenever.PotentialDstBugWarning)
Raised when an [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) operation may
silently preserve an incorrect UTC offset.
A fixed UTC offset (e.g. `+02:00`) carries no timezone rules — it doesn’t
know about DST, historical offset changes, or future policy decisions.
After shifting, rounding, or replacing fields of an
[`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime), the original offset is kept verbatim.
If the region’s rules changed since that offset was recorded, the result
is a timestamp that is off by the difference — silently.
### When it can occur
```python
from whenever import OffsetDateTime
# Denver is UTC-7 in winter, UTC-6 in summer.
# On 2024-03-10, clocks spring forward at 2:00 AM.
d = OffsetDateTime(2024, 3, 9, 13, offset=-7)
d.add(hours=24) # StaleOffsetWarning
# OffsetDateTime("2024-03-10 13:00:00-07:00")
# ^^ -07:00 is wrong; Denver is -06:00 on this date
```
### How to fix it
Convert to [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) first so the offset updates
automatically with the timezone rules:
```python
d.assume_tz("America/Denver").add(hours=24)
# ZonedDateTime("2024-03-10 14:00:00-06:00[America/Denver]") ✓
```
To suppress when the fixed offset is deliberate and known to be correct,
pass `stale_offset_ok=True` (or use Python’s standard warning filters):
```python
d.add(hours=24, stale_offset_ok=True)
```
### *exception* whenever.PotentialDstBugWarning
Bases: [`WheneverWarning`](reference/exceptions.md#whenever.WheneverWarning)
Base class for warnings about potential DST-related bugs in user code.
Not raised directly. Subclasses cover three distinct scenarios:
- [`DaysAssumed24HoursWarning`](reference/exceptions.md#whenever.DaysAssumed24HoursWarning) — days treated as exact 24-hour units
- [`StaleOffsetWarning`](reference/exceptions.md#whenever.StaleOffsetWarning) — fixed offset may be wrong after a DST shift
- [`NaiveArithmeticWarning`](reference/exceptions.md#whenever.NaiveArithmeticWarning) — exact-time arithmetic without timezone context
Catching or filtering this base class handles all three at once:
```python
import warnings, whenever
warnings.filterwarnings("error", category=whenever.PotentialDstBugWarning)
```
### *exception* whenever.WheneverDeprecationWarning
Bases: [`WheneverWarning`](reference/exceptions.md#whenever.WheneverWarning)
Raised when a deprecated feature of the `whenever` library is used.
This is a custom warning class (not a subclass of
[`DeprecationWarning`](https://docs.python.org/3/library/exceptions.html#DeprecationWarning)) so that deprecation warnings from this
library are visible by default—unlike standard `DeprecationWarning`,
which Python silences in production code.
## Exceptions
### *exception* whenever.RepeatedTime
Bases: [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)
A datetime is repeated in a timezone, e.g. because of DST
### *exception* whenever.SkippedTime
Bases: [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)
A datetime is skipped in a timezone, e.g. because of DST
### *exception* whenever.InvalidOffsetError
Bases: [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)
A string has an invalid offset for the given zone
### *exception* whenever.TimeZoneNotFoundError
Bases: [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)
A timezone with the given ID was not found
# reference/instant.md
# `Instant`
### *class* whenever.Instant(arg: [str](https://docs.python.org/3/library/stdtypes.html#str) | [datetime](https://docs.python.org/3/library/datetime.html#datetime.datetime),)
A moment in time, independent of any timezone or calendar.
This is the right type when you only care about *when* something happened,
not the local date or time. It maps 1:1 to a UNIX timestamp.
```pycon
>>> 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
```
Can also be constructed from an ISO 8601 string, a UNIX timestamp,
or a standard library [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime):
```pycon
>>> Instant("2022-10-24T17:00:00Z")
Instant("2022-10-24 17:00:00Z")
```
Convert to other types for local date/time information:
```pycon
>>> py311_release.to_tz("US/Pacific")
ZonedDateTime("2022-10-24 10:00:00-07:00[US/Pacific]")
```
#### NOTE
Although the debug representation uses UTC, `Instant` does *not* have
`.year`, `.hour`, or other calendar attributes—it is not a UTC datetime.
See the [FAQ](https://whenever.rtfd.io/en/latest/faq.html#why-doesn-t-instant-have-year-hour-etc).
#### *classmethod* from_py_datetime(d: [datetime](https://docs.python.org/3/library/datetime.html#datetime.datetime),) → \_T
Create an instance from a [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime) object.
#### Deprecated
Deprecated since version 0.10.0: Use the constructor instead (e.g. `Instant(d)`,
`ZonedDateTime(d)`, etc.)
#### 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`](reference/zoned_datetime.md#whenever.ZonedDateTime)
requires a [`ZoneInfo`](https://docs.python.org/3/library/zoneinfo.html#zoneinfo.ZoneInfo) tzinfo.
#### WARNING
No exceptions are raised if the datetime is ambiguous.
Its `fold` attribute is used to disambiguate.
#### *classmethod* from_timestamp(i: [int](https://docs.python.org/3/library/functions.html#int) | [float](https://docs.python.org/3/library/functions.html#float),) → [Instant](reference/instant.md#whenever.Instant)
Create an Instant from a UNIX timestamp (in seconds).
The inverse of the `timestamp()` method.
#### *classmethod* from_timestamp_millis(i: [int](https://docs.python.org/3/library/functions.html#int),) → [Instant](reference/instant.md#whenever.Instant)
Create an Instant from a UNIX timestamp (in milliseconds).
The inverse of the `timestamp_millis()` method.
#### *classmethod* from_timestamp_nanos(i: [int](https://docs.python.org/3/library/functions.html#int),) → [Instant](reference/instant.md#whenever.Instant)
Create an Instant from a UNIX timestamp (in nanoseconds).
The inverse of the `timestamp_nanos()` method.
#### *classmethod* from_utc(year: [int](https://docs.python.org/3/library/functions.html#int), month: [int](https://docs.python.org/3/library/functions.html#int), day: [int](https://docs.python.org/3/library/functions.html#int), hour: [int](https://docs.python.org/3/library/functions.html#int) = 0, minute: [int](https://docs.python.org/3/library/functions.html#int) = 0, second: [int](https://docs.python.org/3/library/functions.html#int) = 0, , nanosecond: [int](https://docs.python.org/3/library/functions.html#int) = 0) → [Instant](reference/instant.md#whenever.Instant)
Create an Instant defined by a UTC date and time.
#### *classmethod* now() → [Instant](reference/instant.md#whenever.Instant)
Create an Instant from the current time.
```pycon
>>> Instant.now()
Instant("2024-06-15 12:34:56.789123456Z")
```
#### *classmethod* parse(s: [str](https://docs.python.org/3/library/stdtypes.html#str), , , format: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [Instant](reference/instant.md#whenever.Instant)
Parse an instant from a custom pattern string.
The pattern **must** include an offset field (`x`/`X`)
to unambiguously identify the instant.
See [Pattern format](reference/pattern-format.md#pattern-format) for details.
#### TIP
If your input string doesn’t include an offset, parse it with
[`PlainDateTime.parse()`](reference/plain_datetime.md#whenever.PlainDateTime.parse) first, then convert using
[`assume_utc()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_utc) or
[`assume_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_tz).
```pycon
>>> Instant.parse("2024-03-15 14:30Z", format="YYYY-MM-DD hh:mmXXX")
Instant("2024-03-15 14:30:00Z")
>>> Instant.parse("2024-03-15 14:30+05:30", format="YYYY-MM-DD hh:mmxxx")
Instant("2024-03-15 09:00:00Z")
```
#### *classmethod* parse_iso(s: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [Instant](reference/instant.md#whenever.Instant)
Parse an ISO 8601 string. Supports basic and extended formats,
but not week dates or ordinal dates.
See the [docs on ISO8601 support](https://whenever.rtfd.io/en/latest/reference/iso8601.html) for more information.
The inverse of the `format_iso()` method.
#### *classmethod* parse_rfc2822(s: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [Instant](reference/instant.md#whenever.Instant)
Parse a UTC datetime in RFC 2822 format.
The inverse of the `format_rfc2822()` method.
```pycon
>>> Instant.parse_rfc2822("Sat, 15 Aug 2020 23:12:00 GMT")
Instant("2020-08-15 23:12:00Z")
```
```pycon
>>> # 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_\_(delta: [TimeDelta](reference/time_delta.md#whenever.TimeDelta)) → [Instant](reference/instant.md#whenever.Instant)
Add a time amount to this datetime.
See the [docs on arithmetic](https://whenever.rtfd.io/en/latest/guide/arithmetic.html) for more information.
#### \_\_eq_\_(other: [object](https://docs.python.org/3/library/functions.html#object)) → [bool](https://docs.python.org/3/library/functions.html#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()`](reference/instant.md#whenever.Instant.exact_eq).
```pycon
>>> 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
```
#### \_\_format_\_(spec: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [str](https://docs.python.org/3/library/stdtypes.html#str)
Default object formatter.
Return str(self) if format_spec is empty. Raise TypeError otherwise.
#### \_\_ge_\_(other: [Instant](reference/instant.md#whenever.Instant) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) | [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare two datetimes by when they occur in time
`a >= b` is equivalent to `a.to_instant() >= b.to_instant()`
```pycon
>>> OffsetDateTime(2020, 8, 15, hour=19, offset=-8) >= (
... ZoneDateTime(2020, 8, 15, hour=20, tz="Europe/Amsterdam")
... )
True
```
#### \_\_gt_\_(other: [Instant](reference/instant.md#whenever.Instant) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) | [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare two datetimes by when they occur in time
`a > b` is equivalent to `a.to_instant() > b.to_instant()`
```pycon
>>> OffsetDateTime(2020, 8, 15, hour=19, offset=-8) > (
... ZoneDateTime(2020, 8, 15, hour=20, tz="Europe/Amsterdam")
... )
True
```
#### \_\_le_\_(other: [Instant](reference/instant.md#whenever.Instant) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) | [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare two datetimes by when they occur in time
`a <= b` is equivalent to `a.to_instant() <= b.to_instant()`
```pycon
>>> OffsetDateTime(2020, 8, 15, hour=23, offset=8) <= (
... ZoneDateTime(2020, 8, 15, hour=20, tz="Europe/Amsterdam")
... )
True
```
#### \_\_lt_\_(other: [Instant](reference/instant.md#whenever.Instant) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) | [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare two datetimes by when they occur in time
`a < b` is equivalent to `a.to_instant() < b.to_instant()`
```pycon
>>> OffsetDateTime(2020, 8, 15, hour=23, offset=8) < (
... ZoneDateTime(2020, 8, 15, hour=20, tz="Europe/Amsterdam")
... )
True
```
#### \_\_str_\_() → [str](https://docs.python.org/3/library/stdtypes.html#str)
Return str(self).
#### \_\_sub_\_(other: [Instant](reference/instant.md#whenever.Instant) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) | [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
#### \_\_sub_\_(other: [TimeDelta](reference/time_delta.md#whenever.TimeDelta)) → [Instant](reference/instant.md#whenever.Instant)
Subtract another exact time or timedelta
See the [docs on arithmetic](https://whenever.rtfd.io/en/latest/guide/arithmetic.html) for more information.
```pycon
>>> 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)
```
#### add(d: [TimeDelta](reference/time_delta.md#whenever.TimeDelta),) → [Instant](reference/instant.md#whenever.Instant)
#### add(, weeks: [float](https://docs.python.org/3/library/functions.html#float) = 0, days: [float](https://docs.python.org/3/library/functions.html#float) = 0, hours: [float](https://docs.python.org/3/library/functions.html#float) = 0, minutes: [float](https://docs.python.org/3/library/functions.html#float) = 0, seconds: [float](https://docs.python.org/3/library/functions.html#float) = 0, milliseconds: [float](https://docs.python.org/3/library/functions.html#float) = 0, microseconds: [float](https://docs.python.org/3/library/functions.html#float) = 0, nanoseconds: [int](https://docs.python.org/3/library/functions.html#int) = 0, days_assumed_24h_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = UNSET) → [Instant](reference/instant.md#whenever.Instant)
Add a time amount to this instant.
See the [docs on arithmetic](https://whenever.rtfd.io/en/latest/guide/arithmetic.html) for more information.
#### difference(other: [Instant](reference/instant.md#whenever.Instant) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) | [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime),) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
Calculate the exact time difference between two datetimes.
This method returns the exact elapsed [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) between
two instants in time. Equivalent to the subtraction operator (`-`).
Use [`since()`](reference/zoned_datetime.md#whenever.ZonedDateTime.since) or
[`until()`](reference/zoned_datetime.md#whenever.ZonedDateTime.until) for more advanced
options such as calendar units, unit decomposition, and rounding.
#### exact_eq(other: \_T,) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare objects by their values
(instead of whether they represent the same instant).
Different types are never equal.
```pycon
>>> 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
```
#### NOTE
If `a.exact_eq(b)` is true, then
`a == b` is also true, but the converse is not necessarily true.
#### format(pattern: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [str](https://docs.python.org/3/library/stdtypes.html#str)
Format as a custom pattern string.
Instant formats as UTC; See [Pattern format](reference/pattern-format.md#pattern-format) for details.
```pycon
>>> Instant.from_utc(2024, 3, 15, 14, 30).format("YYYY-MM-DD hh:mm:ssXXX")
'2024-03-15 14:30:00Z'
```
#### format_iso(, unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond', 'auto'] = 'auto', basic: [bool](https://docs.python.org/3/library/functions.html#bool) = False, sep: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['T', ' '] = 'T') → [str](https://docs.python.org/3/library/stdtypes.html#str)
Convert to the ISO 8601 string representation.
The inverse of the `parse_iso()` method.
#### format_rfc2822() → [str](https://docs.python.org/3/library/stdtypes.html#str)
Format as an RFC 2822 string.
The inverse of the `parse_rfc2822()` method.
```pycon
>>> Instant.from_utc(2020, 8, 8, hour=23, minute=12).format_rfc2822()
"Sat, 08 Aug 2020 23:12:00 GMT"
```
#### NOTE
The output is also compatible with the (stricter) RFC 9110 standard.
#### py_datetime() → [datetime](https://docs.python.org/3/library/datetime.html#datetime.datetime)
Convert to a standard library [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime)
#### Deprecated
Deprecated since version 0.10.0: Use [`to_stdlib()`](reference/instant.md#whenever.Instant.to_stdlib) instead.
#### round(unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'] | [TimeDelta](reference/time_delta.md#whenever.TimeDelta) = 'second', , , increment: [int](https://docs.python.org/3/library/functions.html#int) = 1, mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = 'half_even') → [Instant](reference/instant.md#whenever.Instant)
Round the instant to the specified unit and increment,
or to a multiple of a [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta).
Various rounding modes are available.
```pycon
>>> 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")
>>> Instant.from_utc(2020, 1, 1, 12, 39, 59).round(TimeDelta(minutes=15))
Instant("2020-01-01 12:45:00Z")
```
#### subtract(d: [TimeDelta](reference/time_delta.md#whenever.TimeDelta),) → [Instant](reference/instant.md#whenever.Instant)
#### subtract(, weeks: [float](https://docs.python.org/3/library/functions.html#float) = 0, days: [float](https://docs.python.org/3/library/functions.html#float) = 0, hours: [float](https://docs.python.org/3/library/functions.html#float) = 0, minutes: [float](https://docs.python.org/3/library/functions.html#float) = 0, seconds: [float](https://docs.python.org/3/library/functions.html#float) = 0, milliseconds: [float](https://docs.python.org/3/library/functions.html#float) = 0, microseconds: [float](https://docs.python.org/3/library/functions.html#float) = 0, nanoseconds: [int](https://docs.python.org/3/library/functions.html#int) = 0, days_assumed_24h_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = UNSET) → [Instant](reference/instant.md#whenever.Instant)
Subtract a time amount from this instant.
See the [docs on arithmetic](https://whenever.rtfd.io/en/latest/guide/arithmetic.html) for more information.
#### timestamp() → [int](https://docs.python.org/3/library/functions.html#int)
The UNIX timestamp for this datetime. Inverse of [`from_timestamp()`](reference/instant.md#whenever.Instant.from_timestamp).
```pycon
>>> Instant.from_utc(1970, 1, 1).timestamp()
0
>>> ts = 1_123_000_000
>>> Instant.from_timestamp(ts).timestamp() == ts
True
```
#### 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.
#### timestamp_millis() → [int](https://docs.python.org/3/library/functions.html#int)
Like [`timestamp()`](reference/instant.md#whenever.Instant.timestamp), but with millisecond precision.
#### timestamp_nanos() → [int](https://docs.python.org/3/library/functions.html#int)
Like [`timestamp()`](reference/instant.md#whenever.Instant.timestamp), but with nanosecond precision.
#### to_fixed_offset(offset: [int](https://docs.python.org/3/library/functions.html#int) | [TimeDelta](reference/time_delta.md#whenever.TimeDelta) = ...,) → [OffsetDateTime](reference/offset_datetime.md#whenever.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_stdlib() → [datetime](https://docs.python.org/3/library/datetime.html#datetime.datetime)
Convert to a standard library [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime)
#### NOTE
- Nanoseconds are truncated to microseconds.
If you wish to customize the rounding behavior, use
the `round()` method first.
- For [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) linked to a system timezone without a
IANA timezone ID, the returned Python datetime will have
a fixed offset ([`timezone`](https://docs.python.org/3/library/datetime.html#datetime.timezone) tzinfo)
#### to_system_tz() → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Convert to a ZonedDateTime of the system’s timezone.
#### to_tz(tz: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Convert to a ZonedDateTime that represents the same moment in time.
* **Raises:**
[**TimeZoneNotFoundError**](reference/exceptions.md#whenever.TimeZoneNotFoundError) – If the timezone ID is not found in the timezone database.
#### MAX *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[Instant](reference/instant.md#whenever.Instant)]* *= Instant("9999-12-31 23:59:59.999999999Z")*
The maximum representable instant.
#### MIN *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[Instant](reference/instant.md#whenever.Instant)]* *= Instant("0001-01-01 00:00:00Z")*
The minimum representable instant.
# reference/iso8601.md
# ISO 8601
[ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) is an international
standard for representing dates and times.
`whenever` provides support for parsing and formatting ISO 8601 strings
for all date/time and duration classes.
## How to convert
All classes have a canonical ISO 8601 representation.
Their constructors accept ISO 8601 strings as input,
and calling `str()` on an instance returns its ISO 8601 representation:
```python
>>> from whenever import Date, Instant
>>> d = Date("2026-01-23") # parsing
>>> str(Instant.now()) # formatting
"2026-01-23T05:30:15.149822Z"
```
This makes all types easily round-trippable to text,
and thus suitable for lossless serialization, e.g. in JSON or databases.
In addition to the constructor and `str()`,
there are also dedicated `parse_iso()` and `format_iso()` methods to customize things.
Below is a summary of the canonical ISO 8601 representations,
along with the corresponding formatter and parser methods which document the details for each class.
| class | Canonical string example | Formatter | Parser |
|-----------------------------------------------------------------------------------------------|--------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------|
| [`Instant`](reference/instant.md#whenever.Instant) | `2026-01-23T05:30:15Z` | [`format_iso()`](reference/instant.md#whenever.Instant.format_iso) | [`parse_iso()`](reference/instant.md#whenever.Instant.parse_iso) |
| [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) | `2026-01-23T14:30:15+09:00[Asia/Tokyo]` [1](#id2) | [`format_iso()`](reference/zoned_datetime.md#whenever.ZonedDateTime.format_iso) | [`parse_iso()`](reference/zoned_datetime.md#whenever.ZonedDateTime.parse_iso) |
| [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) | `2026-01-23T14:30:15+09:00` | [`format_iso()`](reference/offset_datetime.md#whenever.OffsetDateTime.format_iso) | [`parse_iso()`](reference/offset_datetime.md#whenever.OffsetDateTime.parse_iso) |
| [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) | `2026-01-23T14:30:15` | [`format_iso()`](reference/plain_datetime.md#whenever.PlainDateTime.format_iso) | [`parse_iso()`](reference/plain_datetime.md#whenever.PlainDateTime.parse_iso) |
| | | | |
| [`Date`](reference/date.md#whenever.Date) | `2026-01-23` | [`format_iso()`](reference/date.md#whenever.Date.format_iso) | [`parse_iso()`](reference/date.md#whenever.Date.parse_iso) |
| [`Time`](reference/time.md#whenever.Time) | `14:30:15` | [`format_iso()`](reference/time.md#whenever.Time.format_iso) | [`parse_iso()`](reference/time.md#whenever.Time.parse_iso) |
| | | | |
| [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) | `PT48H3M4S` | [`format_iso()`](reference/time_delta.md#whenever.TimeDelta.format_iso) | [`parse_iso()`](reference/time_delta.md#whenever.TimeDelta.parse_iso) |
| [`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta) | `P2Y3M5DT4H30M` | [`format_iso()`](reference/itemized_delta.md#whenever.ItemizedDelta.format_iso) | [`parse_iso()`](reference/itemized_delta.md#whenever.ItemizedDelta.parse_iso) |
| [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) | `P2Y3M5D` | [`format_iso()`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta.format_iso) | [`parse_iso()`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta.parse_iso) |
| | | | |
| [`YearMonth`](reference/yearmonth.md#whenever.YearMonth) | `2026-01` | [`format_iso()`](reference/yearmonth.md#whenever.YearMonth.format_iso) | [`parse_iso()`](reference/yearmonth.md#whenever.YearMonth.parse_iso) |
| [`MonthDay`](reference/monthday.md#whenever.MonthDay) | `--01-23` | [`format_iso()`](reference/monthday.md#whenever.MonthDay.format_iso) | [`parse_iso()`](reference/monthday.md#whenever.MonthDay.parse_iso) |
| [`IsoWeekDate`](reference/isoweekdate.md#whenever.IsoWeekDate) | `2026-W04-5` | [`format_iso()`](reference/isoweekdate.md#whenever.IsoWeekDate.format_iso) | [`parse_iso()`](reference/isoweekdate.md#whenever.IsoWeekDate.parse_iso) |
The `repr()` of each class shows the ISO 8601 representation for easy debugging,
and can even be used to recreate the instance:
```python
>>> from whenever import OffsetDateTime
>>> odt = OffsetDateTime("2026-01-23T14:30:15+09:00")
OffsetDateTime("2026-01-23 14:30:15+09:00")
>>> eval(repr(odt)) == odt # of course, use eval() with caution
True
```
## The “basic” format
By default, the ISO 8601 format uses separators such as hyphens and colons
to improve human readability. This is called the “extended” format.
ISO 8601 also defines a “basic” format without separators.
This format is less human-readable, but commonly used in filenames and
identifiers where special characters may be problematic.
The basic format is supported by all parsers,
and can be produced by the formatters by passing `basic=True`.
```python
>>> from whenever import Instant
>>> i = Instant("2026-01-23T05:30:15Z")
>>> i.format_iso(basic=True)
"20260123T053015Z"
```
## What can be parsed?
As you may or may not know, ISO 8601 is a large and complex standard.
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.
`whenever`’s parsing behavior takes
mostly [after Temporal](https://tc39.es/proposal-temporal/#sec-temporal-iso8601grammar),
namely:
- Both “extended” (e.g. `2023-12-28`) and “basic” (e.g. `20231228`) formats are supported.
- Week and ordinal date formats are *not* supported by the [`Date`](reference/date.md#whenever.Date) and datetime parsers:
e.g. `2023-W52-5` or `2023-365`. Week dates can be parsed separately with [`IsoWeekDate`](reference/isoweekdate.md#whenever.IsoWeekDate).
- 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.
- ISO designator characters may be lowercase or uppercase (e.g. `2023-12-28T11:30:00Z`
is the same as `2023-12-28t11:30:00z`). IANA timezone identifiers remain case-sensitive.
- Only seconds may be fractional (e.g. `11:30:00.123456789Z` is OK but `11:30.5` is not).
- Fractional seconds may contain 1–9 digits (nanosecond precision).
- Both `.` and `,` may be used as decimal separators
- Hours range from `00` through `23`; the end-of-day notation `24:00` is not supported.
- The offset `-00:00` is allowed, and is equivalent to `+00:00`
- The `Z` suffix, which denotes UTC, is accepted for [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime)
and treated as `+00:00`.
- Offsets may be specified up to second-level precision (e.g. `2023-12-28T11:30:00+01:23:45`).
- An IANA timezone identifier may be included in square brackets,
like `2023-12-28T11:30:00+01[Europe/Paris]`. For [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime),
the identifier is required but the preceding offset is optional.
This annotation is part of the recent RFC 9557 extension to RFC 3339.
- In the duration format, the `W` unit may be used alongside other calendar units
(`Y`, `M`, `D`).
- A second value of `60` (leap second) is accepted and normalized to `59`.
See [Are leap seconds supported?](faq.md#faq-leap-seconds) for details.
## Formatting options
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:
```python
>>> 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.
```python
>>> 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.
```python
>>> i.format_iso(sep=" ")
'2025-09-28 21:24:17.664328Z'
```
- `tz` is supported by [`ZonedDateTime.format_iso()`](reference/zoned_datetime.md#whenever.ZonedDateTime.format_iso) and 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.
```python
>>> 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'
```
---
* **[1]** The IANA timezone identifier in square brackets is part of the recent RFC 9557 extension to RFC 3339. It may not be supported by other systems.
# reference/isoweekdate.md
# `IsoWeekDate`
### *class* whenever.IsoWeekDate(iso_string: [str](https://docs.python.org/3/library/stdtypes.html#str),)
### *class* whenever.IsoWeekDate(year: [int](https://docs.python.org/3/library/functions.html#int), week: [int](https://docs.python.org/3/library/functions.html#int), weekday: [Weekday](reference/other-types.md#whenever.Weekday),)
An ISO 8601 week date—a year, week number, and weekday.
The ISO week year may differ from the Gregorian year at year boundaries.
```pycon
>>> iwd = IsoWeekDate(2024, 1, Weekday.MONDAY)
IsoWeekDate("2024-W01-1")
```
Can also be constructed from an ISO 8601 string:
```pycon
>>> IsoWeekDate("2024-W01-1")
IsoWeekDate("2024-W01-1")
```
#### *classmethod* parse_iso(s: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [IsoWeekDate](reference/isoweekdate.md#whenever.IsoWeekDate)
Parse an ISO 8601 week date string
```pycon
>>> IsoWeekDate.parse_iso("2024-W01-1")
IsoWeekDate("2024-W01-1")
```
#### \_\_eq_\_(other: [object](https://docs.python.org/3/library/functions.html#object)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare for equality
```pycon
>>> IsoWeekDate(2024, 1, Weekday.MONDAY) == IsoWeekDate(2024, 1, Weekday.MONDAY)
True
```
#### \_\_ge_\_(other: [IsoWeekDate](reference/isoweekdate.md#whenever.IsoWeekDate)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>=value.
#### \_\_gt_\_(other: [IsoWeekDate](reference/isoweekdate.md#whenever.IsoWeekDate)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>value.
#### \_\_le_\_(other: [IsoWeekDate](reference/isoweekdate.md#whenever.IsoWeekDate)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self<=value.
#### \_\_lt_\_(other: [IsoWeekDate](reference/isoweekdate.md#whenever.IsoWeekDate)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>> IsoWeekDate(2025, 1, Weekday.MONDAY).date()
Date("2024-12-30")
```
#### format_iso(, basic: [bool](https://docs.python.org/3/library/functions.html#bool) = False) → [str](https://docs.python.org/3/library/stdtypes.html#str)
Format as an ISO 8601 week date string
```pycon
>>> IsoWeekDate(2024, 1, Weekday.MONDAY).format_iso()
'2024-W01-1'
>>> IsoWeekDate(2024, 1, Weekday.MONDAY).format_iso(basic=True)
'2024W011'
```
#### replace(, year: [int](https://docs.python.org/3/library/functions.html#int) = ..., week: [int](https://docs.python.org/3/library/functions.html#int) = ..., weekday: [Weekday](reference/other-types.md#whenever.Weekday) = ...) → [IsoWeekDate](reference/isoweekdate.md#whenever.IsoWeekDate)
Return a new [`IsoWeekDate`](reference/isoweekdate.md#whenever.IsoWeekDate) with the given fields replaced
```pycon
>>> IsoWeekDate(2024, 1, Weekday.MONDAY).replace(week=10)
IsoWeekDate("2024-W10-1")
```
#### weeks_in_year() → [int](https://docs.python.org/3/library/functions.html#int)
Number of weeks in this ISO week year (52 or 53)
```pycon
>>> IsoWeekDate(2004, 53, Weekday.FRIDAY).weeks_in_year()
53
>>> IsoWeekDate(2024, 1, Weekday.MONDAY).weeks_in_year()
52
```
#### MAX *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[IsoWeekDate](reference/isoweekdate.md#whenever.IsoWeekDate)]* *= IsoWeekDate("9999-W52-5")*
The maximum possible ISO week date
#### MIN *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[IsoWeekDate](reference/isoweekdate.md#whenever.IsoWeekDate)]* *= IsoWeekDate("0001-W01-1")*
The minimum possible ISO week date
#### *property* week *: [int](https://docs.python.org/3/library/functions.html#int)*
The ISO week number (1–53)
```pycon
>>> IsoWeekDate(2024, 1, Weekday.MONDAY).week
1
```
#### *property* weekday *: [Weekday](reference/other-types.md#whenever.Weekday)*
The day of the week
```pycon
>>> IsoWeekDate(2024, 1, Weekday.MONDAY).weekday
Weekday.MONDAY
```
#### *property* year *: [int](https://docs.python.org/3/library/functions.html#int)*
The ISO week year
```pycon
>>> IsoWeekDate(2024, 1, Weekday.MONDAY).year
2024
```
# reference/itemized_date_delta.md
# `ItemizedDateDelta`
### *class* whenever.ItemizedDateDelta(\*args: [Any](https://docs.python.org/3/library/typing.html#typing.Any), \*\*kwargs: [Any](https://docs.python.org/3/library/typing.html#typing.Any))
A date duration that preserves the exact fields it was created with.
It closely models the ISO 8601 duration format for date-only durations.
```pycon
>>> d = ItemizedDateDelta(years=2, weeks=3)
ItemizedDateDelta("P2Y3W")
>>> d = ItemizedDateDelta("P22W")
>>> str(d)
'P22W'
```
It behaves like a mapping where the keys are
the unit names and the values are the amounts.
Items are ordered from largest to smallest unit.
```pycon
>>> d['weeks']
22
>>> d.get('days')
None
>>> dict(d)
{"years": 2, "weeks": 3}
>>> list(d.keys())
["years", "weeks"]
>>> years, weeks = d.values()
(2, 3)
```
`ItemizedDateDelta` also supports other dictionary-like operations:
```pycon
>>> "days" in d # check for presence of a field
False
>>> len(d) # number of fields set
2
```
Zero values are considered distinct from “missing” values:
```pycon
>>> d2 = ItemizedDateDelta(years=2, weeks=3, days=0)
>>> dict(d2)
{"years": 2, "weeks": 3, "days": 0}
```
Additionally, no normalization is performed.
Months are not rolled into years, weeks into days, etc.
```pycon
>>> d3 = ItemizedDateDelta(months=24, days=100)
ItemizedDateDelta("P24m100d")
```
Empty durations are not allowed. At least one field must be set (but it can be zero):
```pycon
>>> ItemizedDateDelta()
ValueError: At least one field must be set
>>> ItemizedDateDelta(days=0)
ItemizedDateDelta("P0d")
```
Negative durations are supported, but all fields must have the same sign:
```pycon
>>> d4 = ItemizedDateDelta(years=-1, weeks=-2, days=0)
ItemizedDateDelta("-P1y2w0d")
>>> ItemizedDateDelta(years=1, days=-3)
ValueError: All fields must have the same sign
```
#### NOTE
Unlike its predecessor `DateDelta`, `ItemizedDateDelta` does not normalize
its fields. This means that `ItemizedDateDelta(months=14)` and
`ItemizedDateDelta(years=1, months=2)` are considered different values.
To convert to a normalized form, use [`in_units()`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta.in_units).
See also the [delta documentation](https://whenever.rtfd.io/en/latest/guide/deltas.html).
#### *classmethod* parse_iso(s: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta)
Parse the *popular interpretation* of the ISO 8601 duration format.
Inverse of [`format_iso()`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta.format_iso)
```pycon
>>> ItemizedDateDelta.parse_iso("-P1W11D")
ItemizedDateDelta("-P1w11d")
```
You can also use the constructor `ItemizedDateDelta(s)` which is
equivalent to `ItemizedDateDelta.parse_iso(s)`.
#### NOTE
Does not parse all possible ISO 8601 durations. In particular,
it doesn’t allow fractional values.
See [here](reference/deltas.md#iso8601-durations) for more information.
#### \_\_abs_\_() → [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta)
If the contents are negative, return the positive version
```pycon
>>> d = ItemizedDateDelta(weeks=-2, days=-3)
>>> abs(d)
ItemizedDateDelta("P2w3d")
```
#### \_\_bool_\_() → [bool](https://docs.python.org/3/library/functions.html#bool)
An ItemizedDateDelta is considered False if its sign is 0.
```pycon
>>> d = ItemizedDateDelta(weeks=0)
>>> bool(d)
False
>>> d = ItemizedDateDelta(weeks=1)
>>> bool(d)
True
```
#### \_\_contains_\_(key: [object](https://docs.python.org/3/library/functions.html#object)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Check if a specific field is set.
```pycon
>>> d = ItemizedDateDelta(weeks=1, days=0)
>>> "weeks" in d
True
>>> "days" in d
True
>>> "months" in d
False
```
#### \_\_eq_\_(other: [object](https://docs.python.org/3/library/functions.html#object)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare each field for equality, under the following rules:
- No normalization is performed. 12 months is not equal to 1 year, etc.
- Zero values are considered equivalent to missing values.
If you want strict equality (including presence of fields),
use [`exact_eq()`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta.exact_eq).
```pycon
>>> d = ItemizedDateDelta(weeks=2, days=3)
>>> d == ItemizedDateDelta(weeks=2, days=3, months=0)
True
>>> d == ItemizedDateDelta(weeks=2, days=4)
False
```
#### \_\_getitem_\_(key: [DateDeltaUnitStr](reference/other-types.md#whenever.DateDeltaUnitStr)) → [int](https://docs.python.org/3/library/functions.html#int)
Get the value of a specific field by name.
```pycon
>>> d = ItemizedDateDelta(weeks=1, days=0)
>>> d["weeks"]
1
>>> d["days"]
0
>>> d["years"]
KeyError: 'years'
```
#### \_\_iter_\_() → [Iterator](https://docs.python.org/3/library/typing.html#typing.Iterator)[TypeAliasForwardRef('DateDeltaUnitStr')]
Iterate over all unit names for fields that are set, ordered from largest to smallest unit.
#### \_\_len_\_() → [int](https://docs.python.org/3/library/functions.html#int)
Get the number of fields that are set.
```pycon
>>> d = ItemizedDateDelta(weeks=1, days=0)
>>> len(d)
2
```
#### \_\_neg_\_() → [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta)
Invert the sign of the contents
```pycon
>>> d = ItemizedDateDelta(weeks=2, days=3)
>>> -d
ItemizedDateDelta("-P2w3d")
>>> --d
ItemizedDateDelta("P2w3d")
```
#### \_\_str_\_(, lowercase_units: [bool](https://docs.python.org/3/library/functions.html#bool) = False) → [str](https://docs.python.org/3/library/stdtypes.html#str)
Convert to the canionical ISO 8601 string representation:
```text
P(nY)(nM)(nW)(nD)
```
You can also use `str(d)` which is equivalent to `d.format_iso()`.
Inverse of [`parse_iso()`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta.parse_iso).
```pycon
>>> d = ItemizedDateDelta(weeks=1, days=11)
>>> d.format_iso()
'P1W11D'
```
#### NOTE
Negative durations are prefixed with a minus sign,
which is not part of the ISO 8601 standard, but is a common extension.
See [here](reference/deltas.md#iso8601-durations) for more information.
#### add(other: [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta), , , relative_to: [Date](reference/date.md#whenever.Date), in_units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DateDeltaUnitStr')], round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = ..., round_increment: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta)
#### add(other: [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta), , , relative_to: [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime) | [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime), in_units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DeltaUnitStr')], round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = ..., round_increment: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
#### add(, years: [int](https://docs.python.org/3/library/functions.html#int) = ..., months: [int](https://docs.python.org/3/library/functions.html#int) = ..., weeks: [int](https://docs.python.org/3/library/functions.html#int) = ..., days: [int](https://docs.python.org/3/library/functions.html#int) = ..., relative_to: [Date](reference/date.md#whenever.Date), in_units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DateDeltaUnitStr')], round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = ..., round_increment: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta)
#### add(other: [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta), , , cal_unit_composition_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta)
#### add(other: [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta), , , cal_unit_composition_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
#### add(, years: [int](https://docs.python.org/3/library/functions.html#int) = ..., months: [int](https://docs.python.org/3/library/functions.html#int) = ..., weeks: [int](https://docs.python.org/3/library/functions.html#int) = ..., days: [int](https://docs.python.org/3/library/functions.html#int) = ..., cal_unit_composition_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) | [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
Add time to this delta, returning a new delta.
#### exact_eq(other: [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta),) → [bool](https://docs.python.org/3/library/functions.html#bool)
Check for strict equality. All fields *and their presence* must match.
```pycon
>>> d = ItemizedDateDelta(weeks=2, days=3)
>>> d == ItemizedDateDelta(weeks=2, days=3)
True
>>> d == ItemizedDateDelta(weeks=2, days=3, months=0)
True
>>> d.exact_eq(ItemizedDateDelta(weeks=2, days=3, months=0))
False
```
#### format_iso(, lowercase_units: [bool](https://docs.python.org/3/library/functions.html#bool) = False) → [str](https://docs.python.org/3/library/stdtypes.html#str)
Convert to the canionical ISO 8601 string representation:
```text
P(nY)(nM)(nW)(nD)
```
You can also use `str(d)` which is equivalent to `d.format_iso()`.
Inverse of [`parse_iso()`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta.parse_iso).
```pycon
>>> d = ItemizedDateDelta(weeks=1, days=11)
>>> d.format_iso()
'P1W11D'
```
#### NOTE
Negative durations are prefixed with a minus sign,
which is not part of the ISO 8601 standard, but is a common extension.
See [here](reference/deltas.md#iso8601-durations) for more information.
#### get(key: [DateDeltaUnitStr](reference/other-types.md#whenever.DateDeltaUnitStr),) → [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None)
#### get(key: [DateDeltaUnitStr](reference/other-types.md#whenever.DateDeltaUnitStr), default: [int](https://docs.python.org/3/library/functions.html#int),) → [int](https://docs.python.org/3/library/functions.html#int)
Get the value of a specific field by name, or return default if not set.
Part of the mapping protocol
#### in_units(units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DateDeltaUnitStr')], , , relative_to: [Date](reference/date.md#whenever.Date), round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = 'trunc', round_increment: [int](https://docs.python.org/3/library/functions.html#int) = 1) → [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta)
Convert this delta into the specified units. A relative_to date
is required to resolve variable-length units (years and months).
```pycon
>>> d = ItemizedDateDelta(years=1, months=8)
>>> d.in_units(["weeks", "days"], relative_to=Date(2020, 6, 30))
ItemizedDateDelta("P86w6d")
```
#### items() → [ItemsView](https://docs.python.org/3/library/collections.abc.html#collections.abc.ItemsView)[TypeAliasForwardRef('DateDeltaUnitStr'), [int](https://docs.python.org/3/library/functions.html#int)]
Return all defined fields as (unit, value) pairs
ordered from largest to smallest unit.
```pycon
>>> d = ItemizedDateDelta(years=3, days=12, months=0)
>>> list(d.items())
[('years', 3), ('months', 0), ('days', 12)]
```
#### keys() → [KeysView](https://docs.python.org/3/library/collections.abc.html#collections.abc.KeysView)[TypeAliasForwardRef('DateDeltaUnitStr')]
The names of all defined fields, ordered from largest to smallest unit.
Part of the mapping protocol
#### replace(, years: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None) = ..., months: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None) = ..., weeks: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None) = ..., days: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None) = ...) → [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta)
Return a new delta with specific fields replaced.
Fields set to `None` will be removed.
All normal validation rules apply.
```pycon
>>> d = ItemizedDateDelta(years=1, months=2, weeks=3)
>>> d.replace(months=None, weeks=4)
ItemizedDateDelta("P1y4w")
```
#### sign() → [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)[1, 0, -1]
The sign of the delta, whether it’s positive, negative, or zero.
```pycon
>>> ItemizedDateDelta(weeks=2).sign()
1
>>> ItemizedDateDelta(days=-3).sign()
-1
>>> ItemizedDateDelta(weeks=0).sign()
0
```
#### subtract(other: [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta), , , relative_to: [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime) | [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime), in_units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DeltaUnitStr')], round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = ..., round_increment: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
#### subtract(, years: [int](https://docs.python.org/3/library/functions.html#int) = ..., months: [int](https://docs.python.org/3/library/functions.html#int) = ..., weeks: [int](https://docs.python.org/3/library/functions.html#int) = ..., days: [int](https://docs.python.org/3/library/functions.html#int) = ..., relative_to: [Date](reference/date.md#whenever.Date), in_units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DateDeltaUnitStr')], round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = 'trunc', round_increment: [int](https://docs.python.org/3/library/functions.html#int) = 1) → [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta)
#### subtract(other: [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta), , , relative_to: [Date](reference/date.md#whenever.Date), in_units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DateDeltaUnitStr')], round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = 'trunc', round_increment: [int](https://docs.python.org/3/library/functions.html#int) = 1) → [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta)
#### subtract(other: [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta), , , cal_unit_composition_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta)
#### subtract(other: [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta), , , cal_unit_composition_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
Subtract time from this delta, returning a new delta.
#### total(unit: [DateDeltaUnitStr](reference/other-types.md#whenever.DateDeltaUnitStr), , , relative_to: [Date](reference/date.md#whenever.Date)) → [float](https://docs.python.org/3/library/functions.html#float)
Return the total duration expressed in the specified unit as a float
```pycon
>>> ItemizedDateDelta(years=1, months=6).total("months", relative_to=Date(2020, 1, 31))
18.0
>>> ItemizedDateDelta(days=1000).total("years", relative_to=Date(2020, 4, 10))
2.73972602739726
```
#### values() → [ValuesView](https://docs.python.org/3/library/collections.abc.html#collections.abc.ValuesView)[[int](https://docs.python.org/3/library/functions.html#int)]
Return all defined field values, in order
of largest to smallest unit.
```pycon
>>> d = ItemizedDateDelta(years=3, days=12, months=0)
>>> years, months, days = d.values()
(3, 0, 12)
>>> list(d.values())
[3, 0, 12]
```
# reference/itemized_delta.md
# `ItemizedDelta`
### *class* whenever.ItemizedDelta(\*args: [Any](https://docs.python.org/3/library/typing.html#typing.Any), \*\*kwargs: [Any](https://docs.python.org/3/library/typing.html#typing.Any))
A duration that preserves the exact fields it was created with.
It closely models the ISO 8601 duration format for durations.
```pycon
>>> d = ItemizedDelta(weeks=2, days=3, hours=14)
ItemizedDelta("P2w3dT14h")
>>> d = ItemizedDelta("P2w3dT14h")
>>> str(d)
'P2w3dT14h'
```
It behaves like a mapping where the keys are
the unit names and the values are the amounts.
Items are ordered from largest to smallest unit.
```pycon
>>> d['weeks']
2
>>> d.get('minutes')
None
>>> dict(d)
{"weeks": 2, "days": 3, "hours": 14}
>>> list(d.keys())
["weeks", "days", "hours"]
>>> weeks, days, hours = d.values()
(2, 3, 14)
```
`ItemizedDelta` also supports other dictionary-like operations:
```pycon
>>> "months" in d # check for presence of a field
False
>>> len(d) # number of fields set
3
```
Zero values are considered distinct from “missing” values:
```pycon
>>> d2 = ItemizedDelta(years=2, weeks=3, hours=0)
>>> dict(d2)
{"years": 2, "weeks": 3, "hours": 0}
```
Additionally, no normalization is performed.
Months are not rolled into years, minutes into hours, etc.
```pycon
>>> d3 = ItemizedDelta(months=24, minutes=90)
ItemizedDelta("P24mT90m")
```
Empty durations are not allowed. At least one field must be set (but it can be zero):
```pycon
>>> ItemizedDelta()
ValueError: At least one field must be set
>>> ItemizedDelta(seconds=0)
ItemizedDelta("PT0s")
```
Negative durations are supported, but all fields must have the same sign:
```pycon
>>> d4 = ItemizedDelta(years=-1, weeks=-2, days=0)
ItemizedDelta("-P1y2w0d")
>>> ItemizedDelta(years=1, days=-3)
ValueError: All fields must have the same sign
```
#### NOTE
Unlike [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta), `ItemizedDelta` does not normalize
its fields. This means that `ItemizedDelta(hours=90)` and
`ItemizedDelta(days=3, hours=18)` are considered different values.
To convert to a normalized form, use [`in_units()`](reference/itemized_delta.md#whenever.ItemizedDelta.in_units).
See also the [delta documentation](https://whenever.rtfd.io/en/latest/guide/deltas.html).
#### *classmethod* parse_iso(s: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
Parse the *popular interpretation* of the ISO 8601 duration format.
Does not parse all possible ISO 8601 durations.
See [here](reference/deltas.md#iso8601-durations) for more information.
```text
P4D # 4 days
PT4H # 4 hours
PT0M # 0 minutes
PT3M40.5S # 3 minutes and 40.5 seconds
P1W11DT90M # 1 week, 11 days, and 90 minutes
-PT7H400M # -7 hours and -400 minutes
+PT7H4M # 7 hours and 4 minutes (7:04:00)
```
Inverse of [`format_iso()`](reference/itemized_delta.md#whenever.ItemizedDelta.format_iso)
```pycon
>>> ItemizeDelta.parse_iso("-P1W11DT4H")
ItemizeDelta("-P1w11dT4h")
```
#### \_\_abs_\_() → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
If the contents are negative, return the positive version
```pycon
>>> d = ItemizedDelta(weeks=-2, days=-3)
>>> abs(d)
ItemizedDelta("P2w3d")
```
#### \_\_bool_\_() → [bool](https://docs.python.org/3/library/functions.html#bool)
An ItemizedDelta is considered False if its sign is 0.
```pycon
>>> bool(ItemizedDelta(weeks=0))
False
>>> bool(ItemizedDelta(weeks=1))
True
```
#### \_\_contains_\_(key: [object](https://docs.python.org/3/library/functions.html#object)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Check if a specific field is set.
```pycon
>>> d = ItemizedDelta(weeks=1, days=3)
>>> "weeks" in d
True
>>> "hours" in d
False
```
#### \_\_eq_\_(other: [object](https://docs.python.org/3/library/functions.html#object)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare for equality. Each field is individually compared.
No normalization is performed. Zero values are considered equivalent
to missing values.
Thus, `ItemizedDelta(weeks=1, seconds=0) == ItemizedDelta(weeks=1)`
```pycon
>>> d = ItemizedDelta(weeks=2, minutes=90)
>>> d == ItemizedDelta(weeks=2, minutes=90)
True
>>> d == ItemizedDelta(weeks=2, minutes=91)
False
```
If you want strict equality (including presence of fields),
use [`exact_eq()`](reference/itemized_delta.md#whenever.ItemizedDelta.exact_eq).
#### \_\_getitem_\_(key: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [int](https://docs.python.org/3/library/functions.html#int)
Get the value of a specific field by name.
```pycon
>>> d = ItemizedDelta(weeks=1, days=3)
>>> d["weeks"]
1
>>> d["days"]
3
>>> d["hours"]
KeyError: 'hours'
```
#### \_\_iter_\_() → [Iterator](https://docs.python.org/3/library/typing.html#typing.Iterator)[TypeAliasForwardRef('DeltaUnitStr')]
Iterate over all non-missing fields, ordered from largest to smallest unit.
#### \_\_len_\_() → [int](https://docs.python.org/3/library/functions.html#int)
Get the number of fields that are set.
```pycon
>>> d = ItemizedDelta(weeks=1, days=3)
>>> len(d)
2
```
#### \_\_neg_\_() → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
Invert the sign of the contents
```pycon
>>> d = ItemizedDelta(weeks=2, days=3)
>>> -d
ItemizedDelta("-P2w3d")
>>> --d
ItemizedDelta("P2w3d")
```
#### \_\_str_\_(, lowercase_units: [bool](https://docs.python.org/3/library/functions.html#bool) = False) → [str](https://docs.python.org/3/library/stdtypes.html#str)
Format as the *popular interpretation* of the ISO 8601 duration format.
May not strictly adhere to (all versions of) the standard.
See [here](reference/deltas.md#iso8601-durations) for more information.
Inverse of [`parse_iso()`](reference/itemized_delta.md#whenever.ItemizedDelta.parse_iso).
The format is:
```text
P(nY)(nM)(nW)(nD)T(nH)(nM)(nS)
```
```pycon
>>> d = ItemizedDelta(
... weeks=1,
... days=11,
... hours=4,
... seconds=1,
... nanoseconds=12_000,
... )
>>> d.format_iso()
'P1W11DT4H1.000012S'
```
#### add(other: [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta), , , relative_to: [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime), in_units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DeltaUnitStr')], round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = ..., round_increment: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
#### add(, years: [int](https://docs.python.org/3/library/functions.html#int) = ..., months: [int](https://docs.python.org/3/library/functions.html#int) = ..., weeks: [int](https://docs.python.org/3/library/functions.html#int) = ..., days: [int](https://docs.python.org/3/library/functions.html#int) = ..., hours: [int](https://docs.python.org/3/library/functions.html#int) = ..., minutes: [int](https://docs.python.org/3/library/functions.html#int) = ..., seconds: [int](https://docs.python.org/3/library/functions.html#int) = ..., nanoseconds: [int](https://docs.python.org/3/library/functions.html#int) = ..., relative_to: [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime), in_units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DeltaUnitStr')], round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = ..., round_increment: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
#### add(other: [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta), , , cal_unit_composition_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
#### add(other: [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta), , , cal_unit_composition_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
#### add(, years: [int](https://docs.python.org/3/library/functions.html#int) = ..., months: [int](https://docs.python.org/3/library/functions.html#int) = ..., weeks: [int](https://docs.python.org/3/library/functions.html#int) = ..., days: [int](https://docs.python.org/3/library/functions.html#int) = ..., hours: [int](https://docs.python.org/3/library/functions.html#int) = ..., minutes: [int](https://docs.python.org/3/library/functions.html#int) = ..., seconds: [int](https://docs.python.org/3/library/functions.html#int) = ..., nanoseconds: [int](https://docs.python.org/3/library/functions.html#int) = ..., cal_unit_composition_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
Add time to this delta, returning a new delta.
Without a relative_to reference, composition is field-wise and warns
when nonzero calendar units are involved. The warning can be suppressed
with cal_unit_composition_ok=True.
#### date_and_time_parts() → [tuple](https://docs.python.org/3/library/stdtypes.html#tuple)[[ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) | [None](https://docs.python.org/3/library/constants.html#None), [TimeDelta](reference/time_delta.md#whenever.TimeDelta) | [None](https://docs.python.org/3/library/constants.html#None)]
Split into date and time parts.
Either part may be None if no fields were set of that type.
At least one part will be non-None, since at least one field must be set.
```pycon
>>> d = ItemizedDelta(
... years=1,
... months=2,
... weeks=3,
... days=4,
... hours=5,
... minutes=6,
... seconds=7,
... nanoseconds=8,
... )
>>> date_part, time_part = d.date_and_time_parts()
>>> date_part
ItemizedDateDelta("P1y2m3w4d")
>>> time_part
TimeDelta("P5h6m7.000000008s")
>>> ItemizedDelta(weeks=2).date_and_time_parts()
(ItemizedDateDelta("P2w"), None)
```
#### exact_eq(other: [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta),) → [bool](https://docs.python.org/3/library/functions.html#bool)
Check for strict equality. All fields *and their presence* must match.
#### format_iso(, lowercase_units: [bool](https://docs.python.org/3/library/functions.html#bool) = False) → [str](https://docs.python.org/3/library/stdtypes.html#str)
Format as the *popular interpretation* of the ISO 8601 duration format.
May not strictly adhere to (all versions of) the standard.
See [here](reference/deltas.md#iso8601-durations) for more information.
Inverse of [`parse_iso()`](reference/itemized_delta.md#whenever.ItemizedDelta.parse_iso).
The format is:
```text
P(nY)(nM)(nW)(nD)T(nH)(nM)(nS)
```
```pycon
>>> d = ItemizedDelta(
... weeks=1,
... days=11,
... hours=4,
... seconds=1,
... nanoseconds=12_000,
... )
>>> d.format_iso()
'P1W11DT4H1.000012S'
```
#### get(key: [DeltaUnitStr](reference/other-types.md#whenever.DeltaUnitStr),) → [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None)
#### get(key: [DeltaUnitStr](reference/other-types.md#whenever.DeltaUnitStr), default: [int](https://docs.python.org/3/library/functions.html#int),) → [int](https://docs.python.org/3/library/functions.html#int)
Get the value of a specific field by name, or return default if not set.
Part of the mapping protocol
#### in_units(units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DeltaUnitStr')], , , relative_to: [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime) | [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime), round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = 'trunc', round_increment: [int](https://docs.python.org/3/library/functions.html#int) = 1) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
Convert this delta into the specified units. A relative_to datetime
is required to resolve calendar units.
```pycon
>>> d = ItemizedDelta(years=1, months=8, minutes=1000)
>>> d.in_units(["weeks", "hours"], relative_to=ZonedDateTime(2020, 6, 30, 12, tz="Asia/Tokyo"))
ItemizedDelta("P86w160h")
```
* **Parameters:**
**relative_to** –
A [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime), [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime), or
[`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) reference point.
- [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime): DST-aware; emits no warning
- [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime): emits [`NaiveArithmeticWarning`](reference/exceptions.md#whenever.NaiveArithmeticWarning)
when the conversion crosses the calendar/exact-time boundary
(i.e. the delta or output mixes calendar and exact-time units).
Pure calendar-to-calendar or exact-to-exact conversions do not warn.
- [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime): emits [`StaleOffsetWarning`](reference/exceptions.md#whenever.StaleOffsetWarning)
when the delta contains calendar units (years, months, weeks, days)
**or** the output units include calendar units
#### items() → [ItemsView](https://docs.python.org/3/library/collections.abc.html#collections.abc.ItemsView)[TypeAliasForwardRef('DeltaUnitStr'), [int](https://docs.python.org/3/library/functions.html#int)]
Return all defined fields as (unit, value) pairs
ordered from largest to smallest unit.
```pycon
>>> d = ItemizedDelta(years=3, hours=12, days=0)
>>> list(d.items())
[('years', 3), ('days', 0), ('hours', 12)]
```
Part of the mapping protocol
#### keys() → [KeysView](https://docs.python.org/3/library/collections.abc.html#collections.abc.KeysView)[TypeAliasForwardRef('DeltaUnitStr')]
The names of all defined fields, in order of largest to smallest unit.
Part of the mapping protocol
#### replace(, years: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None) = ..., months: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None) = ..., weeks: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None) = ..., days: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None) = ..., hours: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None) = ..., minutes: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None) = ..., seconds: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None) = ..., nanoseconds: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
Return a new delta with specific fields replaced.
Fields set to `None` will be removed.
All normal validation rules apply.
```pycon
>>> d = ItemizedDelta(years=1, months=2, hours=3)
>>> d.replace(months=None, hours=2)
ItemizedDelta("P1yT2h")
```
#### sign() → [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)[1, 0, -1]
The sign of the delta, 1, 0, or -1
#### subtract(other: [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta), , , relative_to: [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime), in_units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DeltaUnitStr')], round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = ..., round_increment: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
#### subtract(, years: [int](https://docs.python.org/3/library/functions.html#int) = ..., months: [int](https://docs.python.org/3/library/functions.html#int) = ..., weeks: [int](https://docs.python.org/3/library/functions.html#int) = ..., days: [int](https://docs.python.org/3/library/functions.html#int) = ..., hours: [int](https://docs.python.org/3/library/functions.html#int) = ..., minutes: [int](https://docs.python.org/3/library/functions.html#int) = ..., seconds: [int](https://docs.python.org/3/library/functions.html#int) = ..., nanoseconds: [int](https://docs.python.org/3/library/functions.html#int) = ..., relative_to: [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime), in_units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DeltaUnitStr')], round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = ..., round_increment: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
#### subtract(other: [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta), , , cal_unit_composition_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
#### subtract(other: [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta), , , cal_unit_composition_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
#### subtract(, years: [int](https://docs.python.org/3/library/functions.html#int) = ..., months: [int](https://docs.python.org/3/library/functions.html#int) = ..., weeks: [int](https://docs.python.org/3/library/functions.html#int) = ..., days: [int](https://docs.python.org/3/library/functions.html#int) = ..., hours: [int](https://docs.python.org/3/library/functions.html#int) = ..., minutes: [int](https://docs.python.org/3/library/functions.html#int) = ..., seconds: [int](https://docs.python.org/3/library/functions.html#int) = ..., nanoseconds: [int](https://docs.python.org/3/library/functions.html#int) = ..., cal_unit_composition_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
Subtract time from this delta, returning a new delta.
#### total(unit: [DeltaUnitStr](reference/other-types.md#whenever.DeltaUnitStr), , , relative_to: [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime) | [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)) → [float](https://docs.python.org/3/library/functions.html#float)
Return the total duration expressed in the specified unit as a float
* **Parameters:**
**relative_to** –
A [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime), [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime), or
[`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) reference point.
- [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime): DST-aware; emits no warning
- [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime): emits [`NaiveArithmeticWarning`](reference/exceptions.md#whenever.NaiveArithmeticWarning)
when the conversion crosses the calendar/exact-time boundary
(i.e. the delta or target unit mixes calendar and exact-time units).
Pure calendar-to-calendar or exact-to-exact conversions do not warn.
- [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime): emits [`StaleOffsetWarning`](reference/exceptions.md#whenever.StaleOffsetWarning)
when the delta contains calendar units (years, months, weeks, days)
**or** the target unit is a calendar unit
#### values() → [ValuesView](https://docs.python.org/3/library/collections.abc.html#collections.abc.ValuesView)[[int](https://docs.python.org/3/library/functions.html#int)]
Return all defined field values, in order
of largest to smallest unit.
```pycon
>>> d = ItemizedDelta(years=3, hours=12, days=0)
>>> years, days, hours = d.values()
(3, 0, 12)
>>> list(d.values())
[3, 0, 12]
```
Part of the mapping protocol
# reference/misc.md
# Miscellaneous
This section contains API documentation for miscellaneous functions and data
* [Other types](reference/other-types.md)
* [Exceptions and warnings](reference/exceptions.md)
* [Deprecated components](reference/deprecated.md)
## Context managers
### *class* whenever.patch_current_time(dt: [Instant](reference/instant.md#whenever.Instant) | [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime), , , keep_ticking: [bool](https://docs.python.org/3/library/functions.html#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 [`reset_system_tz()`](reference/misc.md#whenever.reset_system_tz).
### Example
```pycon
>>> 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)
```
## Timezone data
### whenever.TZPATH *: [tuple](https://docs.python.org/3/library/stdtypes.html#tuple)[[str](https://docs.python.org/3/library/stdtypes.html#str), ...]*
The paths in which `whenever` will search for timezone data.
By default, this is determined the same way as [`zoneinfo.TZPATH`](https://docs.python.org/3/library/zoneinfo.html#zoneinfo.TZPATH),
although you can override it using [`reset_tzpath()`](reference/misc.md#whenever.reset_tzpath) for `whenever` specifically.
### whenever.clear_tzcache(, only_keys: [Iterable](https://docs.python.org/3/library/typing.html#typing.Iterable)[[str](https://docs.python.org/3/library/stdtypes.html#str)] | [None](https://docs.python.org/3/library/constants.html#None) = None) → [None](https://docs.python.org/3/library/constants.html#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()`](https://docs.python.org/3/library/zoneinfo.html#zoneinfo.ZoneInfo.clear_cache).
### whenever.reset_tzpath(target: [Iterable](https://docs.python.org/3/library/typing.html#typing.Iterable)[[str](https://docs.python.org/3/library/stdtypes.html#str) | [PathLike](https://docs.python.org/3/library/os.html#os.PathLike)[[str](https://docs.python.org/3/library/stdtypes.html#str)]] | [None](https://docs.python.org/3/library/constants.html#None) = None,) → [None](https://docs.python.org/3/library/constants.html#None)
Reset or set the paths in which `whenever` will search for timezone data.
It does not affect the [`zoneinfo`](https://docs.python.org/3/library/zoneinfo.html#module-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()`](reference/misc.md#whenever.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()`](https://docs.python.org/3/library/zoneinfo.html#zoneinfo.reset_tzpath)
### whenever.available_timezones() → [set](https://docs.python.org/3/library/stdtypes.html#set)[[str](https://docs.python.org/3/library/stdtypes.html#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()`](https://docs.python.org/3/library/zoneinfo.html#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()`](https://docs.python.org/3/library/zoneinfo.html#zoneinfo.available_timezones),
unless `whenever` was configured to use a different tzpath
using [`reset_tzpath()`](reference/misc.md#whenever.reset_tzpath).
### whenever.reset_system_tz() → [None](https://docs.python.org/3/library/constants.html#None)
Resets the cached system timezone to the currently set system timezone.
```pycon
>>> os.environ["TZ"] = "America/New_York"
>>> reset_system_tz() # system tz is now New York
>>> os.environ["TZ"] = "Europe/London"
>>> ZonedDateTime.now_in_system_tz() # still uses cached New York tz
ZonedDateTime(2025-06-18 15:11:08-04:00[America/New_York])
>>> reset_system_tz() # system tz is now London
>>> ZonedDateTime.now_in_system_tz()
ZonedDateTime(2025-06-18 20:11:08+01:00[Europe/London])
```
# reference/monthday.md
# `MonthDay`
### *class* whenever.MonthDay(iso_string: [str](https://docs.python.org/3/library/stdtypes.html#str),)
### *class* whenever.MonthDay(month: [int](https://docs.python.org/3/library/functions.html#int), day: [int](https://docs.python.org/3/library/functions.html#int))
A month and day without a year component.
Useful for representing recurring annual events such as
birthdays, holidays, or anniversaries.
```pycon
>>> md = MonthDay(11, 23)
MonthDay("--11-23")
```
Can also be constructed from an ISO 8601 string:
```pycon
>>> MonthDay("--11-23")
MonthDay("--11-23")
```
#### *classmethod* parse_iso(s: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [MonthDay](reference/monthday.md#whenever.MonthDay)
Create from the ISO 8601 format `--MM-DD` or `--MMDD`.
Inverse of [`format_iso()`](reference/monthday.md#whenever.MonthDay.format_iso)
```pycon
>>> MonthDay.parse_iso("--11-23")
MonthDay("--11-23")
```
#### \_\_eq_\_(other: [object](https://docs.python.org/3/library/functions.html#object)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare for equality
```pycon
>>> md = MonthDay(10, 1)
>>> md == MonthDay(10, 1)
True
>>> md == MonthDay(10, 2)
False
```
#### \_\_ge_\_(other: [MonthDay](reference/monthday.md#whenever.MonthDay)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>=value.
#### \_\_gt_\_(other: [MonthDay](reference/monthday.md#whenever.MonthDay)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>value.
#### \_\_le_\_(other: [MonthDay](reference/monthday.md#whenever.MonthDay)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self<=value.
#### \_\_lt_\_(other: [MonthDay](reference/monthday.md#whenever.MonthDay)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>> 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.
#### format_iso() → [str](https://docs.python.org/3/library/stdtypes.html#str)
Format as the ISO 8601 month-day format.
Inverse of `parse_iso`.
```pycon
>>> 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](https://docs.python.org/3/library/functions.html#int),) → [Date](reference/date.md#whenever.Date)
Create a date from this month-day in a given year
```pycon
>>> 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](https://docs.python.org/3/library/functions.html#bool)
Check if the month-day is February 29th
```pycon
>>> MonthDay(2, 29).is_leap()
True
>>> MonthDay(3, 1).is_leap()
False
```
#### replace(month: [int](https://docs.python.org/3/library/functions.html#int) = ..., day: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [MonthDay](reference/monthday.md#whenever.MonthDay)
Create a new instance with the given fields replaced
```pycon
>>> d = MonthDay(11, 23)
>>> d.replace(month=3)
MonthDay("--03-23")
```
#### MAX *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[MonthDay](reference/monthday.md#whenever.MonthDay)]* *= MonthDay("--12-31")*
The maximum possible month-day
#### MIN *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[MonthDay](reference/monthday.md#whenever.MonthDay)]* *= MonthDay("--01-01")*
The minimum possible month-day
#### *property* day *: [int](https://docs.python.org/3/library/functions.html#int)*
The day component of the month-day
```pycon
>>> MonthDay(11, 23).day
23
```
#### *property* month *: [int](https://docs.python.org/3/library/functions.html#int)*
The month component of the month-day
```pycon
>>> MonthDay(11, 23).month
11
```
# reference/offset_datetime.md
# `OffsetDateTime`
### *class* whenever.OffsetDateTime(iso_string: [str](https://docs.python.org/3/library/stdtypes.html#str),)
### *class* whenever.OffsetDateTime(py_dt: [datetime](https://docs.python.org/3/library/datetime.html#datetime.datetime),)
### *class* whenever.OffsetDateTime(year: [int](https://docs.python.org/3/library/functions.html#int), month: [int](https://docs.python.org/3/library/functions.html#int), day: [int](https://docs.python.org/3/library/functions.html#int), hour: [int](https://docs.python.org/3/library/functions.html#int) = 0, minute: [int](https://docs.python.org/3/library/functions.html#int) = 0, second: [int](https://docs.python.org/3/library/functions.html#int) = 0, , nanosecond: [int](https://docs.python.org/3/library/functions.html#int) = 0, offset: [int](https://docs.python.org/3/library/functions.html#int) | [TimeDelta](reference/time_delta.md#whenever.TimeDelta))
A datetime with a fixed UTC offset.
Useful for representing a moment in time together with the local
date and time as observed at that offset. The offset is fixed and
does not account for DST transitions.
```pycon
>>> # Midnight in Salt Lake City
>>> OffsetDateTime(2023, 4, 21, offset=-6)
OffsetDateTime("2023-04-21 00:00:00-06:00")
```
Can also be constructed from an ISO 8601 string
or a standard library [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime):
```pycon
>>> OffsetDateTime("2023-04-21T00:00:00-06:00")
OffsetDateTime("2023-04-21 00:00:00-06:00")
```
Convert to [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) for DST-aware operations:
```pycon
>>> dt = OffsetDateTime(2023, 4, 21, offset=-6)
>>> dt.assume_tz("US/Mountain")
ZonedDateTime("2023-04-21 00:00:00-06:00[US/Mountain]")
```
#### IMPORTANT
Operations that shift, round, or replace fields of this type keep the
original offset, which may become stale if DST rules have changed.
Use [`assume_tz()`](reference/offset_datetime.md#whenever.OffsetDateTime.assume_tz) to convert to a `ZonedDateTime` first if you
need DST-aware arithmetic.
#### *classmethod* from_py_datetime(d: [datetime](https://docs.python.org/3/library/datetime.html#datetime.datetime),) → \_T
Create an instance from a [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime) object.
#### Deprecated
Deprecated since version 0.10.0: Use the constructor instead (e.g. `Instant(d)`,
`ZonedDateTime(d)`, etc.)
#### 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`](reference/zoned_datetime.md#whenever.ZonedDateTime)
requires a [`ZoneInfo`](https://docs.python.org/3/library/zoneinfo.html#zoneinfo.ZoneInfo) tzinfo.
#### WARNING
No exceptions are raised if the datetime is ambiguous.
Its `fold` attribute is used to disambiguate.
#### *classmethod* from_timestamp(i: [int](https://docs.python.org/3/library/functions.html#int) | [float](https://docs.python.org/3/library/functions.html#float), , , offset: [int](https://docs.python.org/3/library/functions.html#int) | [TimeDelta](reference/time_delta.md#whenever.TimeDelta), ignore_dst: [bool](https://docs.python.org/3/library/functions.html#bool) = ..., stale_offset_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
Create an instance from a UNIX timestamp (in seconds).
The inverse of the `timestamp()` method.
#### WARNING
Converting a UNIX timestamp to `OffsetDateTime` with a fixed UTC offset
is correct for that offset, but the offset may be stale for the region you
intend at that timestamp: a fixed offset contains no DST or other timezone
rules. Use
`ZonedDateTime.from_timestamp(ts, tz='')` if you know the timezone,
or `Instant.from_timestamp()` for timezone-agnostic exact time.
Pass `stale_offset_ok=True` to suppress.
#### *classmethod* from_timestamp_millis(i: [int](https://docs.python.org/3/library/functions.html#int), , , offset: [int](https://docs.python.org/3/library/functions.html#int) | [TimeDelta](reference/time_delta.md#whenever.TimeDelta), ignore_dst: [bool](https://docs.python.org/3/library/functions.html#bool) = ..., stale_offset_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
Create an instance from a UNIX timestamp (in milliseconds).
The inverse of the `timestamp_millis()` method.
See [`from_timestamp()`](reference/offset_datetime.md#whenever.OffsetDateTime.from_timestamp) for more information.
#### *classmethod* from_timestamp_nanos(i: [int](https://docs.python.org/3/library/functions.html#int), , , offset: [int](https://docs.python.org/3/library/functions.html#int) | [TimeDelta](reference/time_delta.md#whenever.TimeDelta), ignore_dst: [bool](https://docs.python.org/3/library/functions.html#bool) = ..., stale_offset_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
Create an instance from a UNIX timestamp (in nanoseconds).
The inverse of the `timestamp_nanos()` method.
See [`from_timestamp()`](reference/offset_datetime.md#whenever.OffsetDateTime.from_timestamp) for more information.
#### *classmethod* now(offset: [int](https://docs.python.org/3/library/functions.html#int) | [TimeDelta](reference/time_delta.md#whenever.TimeDelta), , , ignore_dst: [bool](https://docs.python.org/3/library/functions.html#bool) = ..., stale_offset_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
Create an instance from the current time.
#### WARNING
Getting the current time as an `OffsetDateTime` with a fixed UTC offset
is correct for that offset, but the offset may be stale for the region you
intend: fixed offsets don’t update when DST or other timezone rules change.
Use `ZonedDateTime.now('')` if you know the timezone, or
`Instant.now()` for timezone-agnostic exact time.
Pass `stale_offset_ok=True` to suppress.
#### *classmethod* parse(s: [str](https://docs.python.org/3/library/stdtypes.html#str), , , format: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
Parse an offset datetime from a custom pattern string.
The pattern **must** include an offset field (`x`/`X`).
See [Pattern format](reference/pattern-format.md#pattern-format) for details.
#### TIP
If your input string doesn’t include an offset, parse it with
[`PlainDateTime.parse()`](reference/plain_datetime.md#whenever.PlainDateTime.parse) first, then convert using
[`assume_fixed_offset()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_fixed_offset) or
[`assume_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_tz).
```pycon
>>> OffsetDateTime.parse("2024-03-15 14:30+02:00", format="YYYY-MM-DD hh:mmxxx")
OffsetDateTime("2024-03-15 14:30:00+02:00")
```
#### *classmethod* parse_iso(s: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
Parse an ISO 8601 string with a UTC offset.
Supports `YYYY-MM-DDTHH:MM:SS±HH:MM` and variants
(see the [ISO 8601 docs](https://whenever.rtfd.io/en/latest/reference/iso8601.html)
for full details).
The inverse of the `format_iso()` method.
```pycon
>>> OffsetDateTime.parse_iso("2020-08-15T23:12:00+02:00")
OffsetDateTime("2020-08-15 23:12:00+02:00")
```
#### NOTE
`Z` is accepted as an offset and treated as `+00:00`.
Strictly speaking, `Z` means “UTC” (i.e. no fixed offset),
but in practice it is almost universally used as a synonym for `+00:00`.
#### *classmethod* parse_rfc2822(s: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
Parse an offset datetime in RFC 2822 format.
The inverse of the `format_rfc2822()` method.
```pycon
>>> 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](https://docs.python.org/3/library/stdtypes.html#str), , , format: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
Parse a datetime with offset using the standard library `strptime()` method.
#### Deprecated
Deprecated since version 0.10.0: Use [`parse()`](reference/offset_datetime.md#whenever.OffsetDateTime.parse) with a pattern string instead, or use
`OffsetDateTime(datetime.strptime(...))`.
#### \_\_add_\_(delta: [TimeDelta](reference/time_delta.md#whenever.TimeDelta)) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
Add a time delta to this datetime.
#### WARNING
Shifting an `OffsetDateTime` keeps the fixed UTC offset, which may not
match the actual offset after a DST or other timezone transition.
For example, adding 1 day to `2024-03-09 12:00-07:00` gives
`2024-03-10 12:00-07:00`, but if this offset represents Denver,
Colorado (America/Denver), the actual offset changed to `-06:00` that day.
Convert to a `ZonedDateTime` first for timezone-aware arithmetic
using [`assume_tz()`](reference/offset_datetime.md#whenever.OffsetDateTime.assume_tz).
Use `.add(..., stale_offset_ok=True)` or Python’s
standard warning filters to suppress.
#### \_\_eq_\_(other: [object](https://docs.python.org/3/library/functions.html#object)) → [bool](https://docs.python.org/3/library/functions.html#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()`](reference/offset_datetime.md#whenever.OffsetDateTime.exact_eq).
```pycon
>>> 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
```
#### \_\_format_\_(spec: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [str](https://docs.python.org/3/library/stdtypes.html#str)
Default object formatter.
Return str(self) if format_spec is empty. Raise TypeError otherwise.
#### \_\_ge_\_(other: [Instant](reference/instant.md#whenever.Instant) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) | [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare two datetimes by when they occur in time
`a >= b` is equivalent to `a.to_instant() >= b.to_instant()`
```pycon
>>> OffsetDateTime(2020, 8, 15, hour=19, offset=-8) >= (
... ZoneDateTime(2020, 8, 15, hour=20, tz="Europe/Amsterdam")
... )
True
```
#### \_\_gt_\_(other: [Instant](reference/instant.md#whenever.Instant) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) | [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare two datetimes by when they occur in time
`a > b` is equivalent to `a.to_instant() > b.to_instant()`
```pycon
>>> OffsetDateTime(2020, 8, 15, hour=19, offset=-8) > (
... ZoneDateTime(2020, 8, 15, hour=20, tz="Europe/Amsterdam")
... )
True
```
#### \_\_le_\_(other: [Instant](reference/instant.md#whenever.Instant) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) | [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare two datetimes by when they occur in time
`a <= b` is equivalent to `a.to_instant() <= b.to_instant()`
```pycon
>>> OffsetDateTime(2020, 8, 15, hour=23, offset=8) <= (
... ZoneDateTime(2020, 8, 15, hour=20, tz="Europe/Amsterdam")
... )
True
```
#### \_\_lt_\_(other: [Instant](reference/instant.md#whenever.Instant) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) | [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare two datetimes by when they occur in time
`a < b` is equivalent to `a.to_instant() < b.to_instant()`
```pycon
>>> OffsetDateTime(2020, 8, 15, hour=23, offset=8) < (
... ZoneDateTime(2020, 8, 15, hour=20, tz="Europe/Amsterdam")
... )
True
```
#### \_\_str_\_() → [str](https://docs.python.org/3/library/stdtypes.html#str)
Return str(self).
#### \_\_sub_\_(other: [Instant](reference/instant.md#whenever.Instant) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) | [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
#### \_\_sub_\_(other: [TimeDelta](reference/time_delta.md#whenever.TimeDelta)) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
Subtract a time delta or calculate the duration to another exact time.
#### add(delta: [DateTimeDelta](reference/deprecated.md#whenever.DateTimeDelta) | [TimeDelta](reference/time_delta.md#whenever.TimeDelta) | [DateDelta](reference/deprecated.md#whenever.DateDelta) | [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta) | [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta),) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
#### add(, years: [int](https://docs.python.org/3/library/functions.html#int) = 0, months: [int](https://docs.python.org/3/library/functions.html#int) = 0, weeks: [int](https://docs.python.org/3/library/functions.html#int) = 0, days: [int](https://docs.python.org/3/library/functions.html#int) = 0, hours: [float](https://docs.python.org/3/library/functions.html#float) = 0, minutes: [float](https://docs.python.org/3/library/functions.html#float) = 0, seconds: [float](https://docs.python.org/3/library/functions.html#float) = 0, milliseconds: [float](https://docs.python.org/3/library/functions.html#float) = 0, microseconds: [float](https://docs.python.org/3/library/functions.html#float) = 0, nanoseconds: [int](https://docs.python.org/3/library/functions.html#int) = 0, ignore_dst: [bool](https://docs.python.org/3/library/functions.html#bool) = ..., stale_offset_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
Add a time amount to this datetime.
#### WARNING
Shifting an `OffsetDateTime` keeps the fixed UTC offset, which may not
match the actual offset after a DST or other timezone transition.
Convert to a `ZonedDateTime` first for timezone-aware arithmetic
using [`assume_tz()`](reference/offset_datetime.md#whenever.OffsetDateTime.assume_tz).
Pass `stale_offset_ok=True` to suppress;
Python’s standard warning filters also apply.
#### assume_tz(tz: [str](https://docs.python.org/3/library/stdtypes.html#str), , offset_mismatch: [OffsetMismatchStr](reference/other-types.md#whenever.OffsetMismatchStr) = 'raise') → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Associate this offset datetime with a timezone, returning a ZonedDateTime.
This is the inverse of [`ZonedDateTime.to_fixed_offset()`](reference/zoned_datetime.md#whenever.ZonedDateTime.to_fixed_offset).
By default, if the offset of this datetime doesn’t match the actual
offset of the timezone at this datetime, an error is raised.
Using the `offset_mismatch` parameter, you can choose to ignore
the mismatch, keeping either the instant or the local time the same.
#### date() → [Date](reference/date.md#whenever.Date)
The date part of the datetime
```pycon
>>> d = PlaineDateTime("2020-01-02 03:04:05")
>>> d.date()
Date("2021-01-02")
```
To perform the inverse, use [`Date.at()`](reference/date.md#whenever.Date.at) and a method
like [`assume_utc()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_utc) or
[`assume_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_tz):
```pycon
>>> date.at(time).assume_tz("Europe/London")
ZonedDateTime("2021-01-02T03:04:05+00:00[Europe/London]")
```
#### day_of_year() → [int](https://docs.python.org/3/library/functions.html#int)
Ordinal day in the year (1–366)
```pycon
>>> PlainDateTime(2021, 1, 2).day_of_year()
2
```
#### days_in_month() → [int](https://docs.python.org/3/library/functions.html#int)
Number of days in the current month (28–31)
```pycon
>>> PlainDateTime(2024, 2, 1).days_in_month()
29
```
#### days_in_year() → [int](https://docs.python.org/3/library/functions.html#int)
Number of days in the current year (365 or 366)
```pycon
>>> PlainDateTime(2024, 1, 1).days_in_year()
366
```
#### difference(other: [Instant](reference/instant.md#whenever.Instant) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) | [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime),) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
Calculate the exact time difference between two datetimes.
This method returns the exact elapsed [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) between
two instants in time. Equivalent to the subtraction operator (`-`).
Use [`since()`](reference/zoned_datetime.md#whenever.ZonedDateTime.since) or
[`until()`](reference/zoned_datetime.md#whenever.ZonedDateTime.until) for more advanced
options such as calendar units, unit decomposition, and rounding.
#### end_of(unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['year', 'month', 'week_mon', 'week_sun', 'day', 'hour', 'minute', 'second'], , , stale_offset_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
The end of the given unit
```pycon
>>> OffsetDateTime(2024, 8, 15, 14, 30, offset=5).end_of("day")
OffsetDateTime("2024-08-15 23:59:59.999999999+05:00")
```
See also [`start_of()`](reference/offset_datetime.md#whenever.OffsetDateTime.start_of)
#### exact_eq(other: \_T,) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare objects by their values
(instead of whether they represent the same instant).
Different types are never equal.
```pycon
>>> 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
```
#### NOTE
If `a.exact_eq(b)` is true, then
`a == b` is also true, but the converse is not necessarily true.
#### format(pattern: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [str](https://docs.python.org/3/library/stdtypes.html#str)
Format as a custom pattern string.
See [Pattern format](reference/pattern-format.md#pattern-format) for details.
```pycon
>>> OffsetDateTime(2024, 3, 15, 14, 30, offset=hours(2)).format(
... "YYYY-MM-DD hh:mmxxx"
... )
'2024-03-15 14:30+02:00'
```
#### format_iso(, unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond', 'auto'] = 'auto', basic: [bool](https://docs.python.org/3/library/functions.html#bool) = False, sep: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['T', ' '] = 'T') → [str](https://docs.python.org/3/library/stdtypes.html#str)
Convert to the popular ISO format `YYYY-MM-DDTHH:MM:SS±HH:MM`
The inverse of the `parse_iso()` method.
#### format_rfc2822() → [str](https://docs.python.org/3/library/stdtypes.html#str)
Format as an RFC 2822 string.
The inverse of the `parse_rfc2822()` method.
```pycon
>>> OffsetDateTime(2020, 8, 15, 23, 12, offset=hours(2)).format_rfc2822()
"Sat, 15 Aug 2020 23:12:00 +0200"
```
#### in_leap_year() → [bool](https://docs.python.org/3/library/functions.html#bool)
Whether this date’s year is a leap year
```pycon
>>> PlainDateTime(2024, 1, 1).in_leap_year()
True
```
#### py_datetime() → [datetime](https://docs.python.org/3/library/datetime.html#datetime.datetime)
Convert to a standard library [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime)
#### Deprecated
Deprecated since version 0.10.0: Use [`to_stdlib()`](reference/offset_datetime.md#whenever.OffsetDateTime.to_stdlib) instead.
#### replace(year: [int](https://docs.python.org/3/library/functions.html#int) = ..., month: [int](https://docs.python.org/3/library/functions.html#int) = ..., day: [int](https://docs.python.org/3/library/functions.html#int) = ..., hour: [int](https://docs.python.org/3/library/functions.html#int) = ..., minute: [int](https://docs.python.org/3/library/functions.html#int) = ..., second: [int](https://docs.python.org/3/library/functions.html#int) = ..., , nanosecond: [int](https://docs.python.org/3/library/functions.html#int) = ..., offset: [int](https://docs.python.org/3/library/functions.html#int) | [TimeDelta](reference/time_delta.md#whenever.TimeDelta) = ..., ignore_dst: [bool](https://docs.python.org/3/library/functions.html#bool) = ..., stale_offset_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
Construct a new instance with the given fields replaced.
#### WARNING
Replacing fields of an `OffsetDateTime` keeps the fixed UTC offset,
which may no longer be correct after the change (e.g. replacing the month
on a European-timezone datetime may move it into a different DST period).
Convert to `ZonedDateTime` first for timezone-aware field replacement
using [`assume_tz()`](reference/offset_datetime.md#whenever.OffsetDateTime.assume_tz).
Pass `stale_offset_ok=True` to suppress.
#### replace_date(date: [Date](reference/date.md#whenever.Date), , , ignore_dst: [bool](https://docs.python.org/3/library/functions.html#bool) = ..., stale_offset_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
Construct a new instance with the date replaced.
See [`replace()`](reference/offset_datetime.md#whenever.OffsetDateTime.replace) for more information.
#### replace_time(time: [Time](reference/time.md#whenever.Time), , , ignore_dst: [bool](https://docs.python.org/3/library/functions.html#bool) = ..., stale_offset_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
Construct a new instance with the time replaced.
See [`replace()`](reference/offset_datetime.md#whenever.OffsetDateTime.replace) for more information.
#### round(unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['day', 'hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'] | [TimeDelta](reference/time_delta.md#whenever.TimeDelta) = 'second', , , increment: [int](https://docs.python.org/3/library/functions.html#int) = 1, mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = 'half_even', ignore_dst: [bool](https://docs.python.org/3/library/functions.html#bool) = ..., stale_offset_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
Round the datetime to the specified unit and increment,
or to a multiple of a [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta).
Different rounding modes are available.
```pycon
>>> d = OffsetDateTime(2020, 8, 15, 23, 24, 18, offset=+4)
>>> d.round("day")
OffsetDateTime("2020-08-16 00:00:00[+04:00]")
>>> d.round("minute", increment=15, mode="floor")
OffsetDateTime("2020-08-15 23:15:00[+04:00]")
```
#### WARNING
Rounding an `OffsetDateTime` keeps the fixed UTC offset, which may not
be accurate if the rounded datetime crosses into a different DST period.
Convert to a `ZonedDateTime` first for timezone-aware rounding
using [`assume_tz()`](reference/offset_datetime.md#whenever.OffsetDateTime.assume_tz).
Pass `stale_offset_ok=True` to suppress.
#### since(b: [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime), , , total: [DeltaUnitStr](reference/other-types.md#whenever.DeltaUnitStr)) → [float](https://docs.python.org/3/library/functions.html#float)
#### since(b: [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime), , , in_units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DeltaUnitStr')], round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = ..., round_increment: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
Calculate the duration since another OffsetDateTime,
in terms of the specified units.
```pycon
>>> d1 = OffsetDateTime(2020, 8, 15, 23, 12, offset=2)
>>> d2 = OffsetDateTime(2020, 8, 14, 22, offset=2)
>>> d1.since(d2, in_units=["hours", "minutes"],
... round_increment=15,
... round_mode="ceil")
ItemizedDelta("PT25h15m")
```
When calculating calendar units (years, months, weeks, days),
both datetimes must have the same offset.
#### start_of(unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['year', 'month', 'week_mon', 'week_sun', 'day', 'hour', 'minute', 'second'], , , stale_offset_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
The start of the given unit
```pycon
>>> OffsetDateTime(2024, 8, 15, 14, 30, offset=5).start_of("day")
OffsetDateTime("2024-08-15 00:00:00+05:00")
```
#### WARNING
The offset is preserved, which may not be correct for the
resulting time. See [`StaleOffsetWarning`](reference/exceptions.md#whenever.StaleOffsetWarning).
Pass `stale_offset_ok=True` to suppress.
#### subtract(delta: [DateTimeDelta](reference/deprecated.md#whenever.DateTimeDelta) | [TimeDelta](reference/time_delta.md#whenever.TimeDelta) | [DateDelta](reference/deprecated.md#whenever.DateDelta) | [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta) | [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta),) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
#### subtract(, years: [int](https://docs.python.org/3/library/functions.html#int) = 0, months: [int](https://docs.python.org/3/library/functions.html#int) = 0, weeks: [int](https://docs.python.org/3/library/functions.html#int) = 0, days: [int](https://docs.python.org/3/library/functions.html#int) = 0, hours: [float](https://docs.python.org/3/library/functions.html#float) = 0, minutes: [float](https://docs.python.org/3/library/functions.html#float) = 0, seconds: [float](https://docs.python.org/3/library/functions.html#float) = 0, milliseconds: [float](https://docs.python.org/3/library/functions.html#float) = 0, microseconds: [float](https://docs.python.org/3/library/functions.html#float) = 0, nanoseconds: [int](https://docs.python.org/3/library/functions.html#int) = 0, ignore_dst: [bool](https://docs.python.org/3/library/functions.html#bool) = ..., stale_offset_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
Subtract a time amount from this datetime.
See [`add()`](reference/offset_datetime.md#whenever.OffsetDateTime.add) for more information.
#### time() → [Time](reference/time.md#whenever.Time)
The time-of-day part of the datetime
```pycon
>>> d = ZonedDateTime("2021-01-02T03:04:05+01:00[Europe/Paris])"
>>> d.time()
Time(03:04:05)
```
To perform the inverse, use [`Time.on()`](reference/time.md#whenever.Time.on) and a method
like [`assume_utc()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_utc) or
[`assume_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_tz):
```pycon
>>> time.on(date).assume_tz("Europe/Paris")
ZonedDateTime("2021-01-02T03:04:05+01:00[Europe/Paris]")
```
#### timestamp() → [int](https://docs.python.org/3/library/functions.html#int)
The UNIX timestamp for this datetime. Inverse of [`from_timestamp()`](reference/offset_datetime.md#whenever.OffsetDateTime.from_timestamp).
```pycon
>>> Instant.from_utc(1970, 1, 1).timestamp()
0
>>> ts = 1_123_000_000
>>> Instant.from_timestamp(ts).timestamp() == ts
True
```
#### 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.
#### timestamp_millis() → [int](https://docs.python.org/3/library/functions.html#int)
Like [`timestamp()`](reference/offset_datetime.md#whenever.OffsetDateTime.timestamp), but with millisecond precision.
#### timestamp_nanos() → [int](https://docs.python.org/3/library/functions.html#int)
Like [`timestamp()`](reference/offset_datetime.md#whenever.OffsetDateTime.timestamp), but with nanosecond precision.
#### to_fixed_offset(offset: [int](https://docs.python.org/3/library/functions.html#int) | [TimeDelta](reference/time_delta.md#whenever.TimeDelta) = ...,) → [OffsetDateTime](reference/offset_datetime.md#whenever.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_instant() → [Instant](reference/instant.md#whenever.Instant)
Get the underlying instant in time
```pycon
>>> d = ZonedDateTime(2020, 8, 15, hour=23, tz="Europe/Amsterdam")
>>> d.to_instant()
Instant("2020-08-15 21:00:00Z")
```
#### to_plain() → [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)
Get the underlying date and time without offset or timezone
As an inverse, [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) has methods
[`assume_utc()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_utc), [`assume_fixed_offset()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_fixed_offset)
, [`assume_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_tz), and [`assume_system_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_system_tz).
#### to_stdlib() → [datetime](https://docs.python.org/3/library/datetime.html#datetime.datetime)
Convert to a standard library [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime)
#### NOTE
- Nanoseconds are truncated to microseconds.
If you wish to customize the rounding behavior, use
the `round()` method first.
- For [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) linked to a system timezone without a
IANA timezone ID, the returned Python datetime will have
a fixed offset ([`timezone`](https://docs.python.org/3/library/datetime.html#datetime.timezone) tzinfo)
#### to_system_tz() → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Convert to a ZonedDateTime of the system’s timezone.
#### to_tz(tz: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Convert to a ZonedDateTime that represents the same moment in time.
* **Raises:**
[**TimeZoneNotFoundError**](reference/exceptions.md#whenever.TimeZoneNotFoundError) – If the timezone ID is not found in the timezone database.
#### until(b: [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime), , , total: [DeltaUnitStr](reference/other-types.md#whenever.DeltaUnitStr)) → [float](https://docs.python.org/3/library/functions.html#float)
#### until(b: [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime), , , in_units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DeltaUnitStr')], round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = ..., round_increment: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
Inverse of the `since()` method. See [`since()`](reference/offset_datetime.md#whenever.OffsetDateTime.since) for more information.
#### *property* day *: [int](https://docs.python.org/3/library/functions.html#int)*
The day component of the datetime
#### *property* hour *: [int](https://docs.python.org/3/library/functions.html#int)*
The hour component of the datetime
#### *property* minute *: [int](https://docs.python.org/3/library/functions.html#int)*
The minute component of the datetime
#### *property* month *: [int](https://docs.python.org/3/library/functions.html#int)*
The month component of the datetime
#### *property* nanosecond *: [int](https://docs.python.org/3/library/functions.html#int)*
The nanosecond component of the datetime
#### *property* offset *: [TimeDelta](reference/time_delta.md#whenever.TimeDelta)*
The UTC offset of the datetime
#### *property* second *: [int](https://docs.python.org/3/library/functions.html#int)*
The second component of the datetime
#### *property* year *: [int](https://docs.python.org/3/library/functions.html#int)*
The year component of the datetime
# reference/other-types.md
# Other types
This section contains API documentation for choice and other types,
i.e. enums, unions, and literals.
### *class* whenever.Weekday(\*values)
Day of the week; `.value` corresponds with ISO numbering
(monday=1, sunday=7).
All members are also available as constants in the module namespace:
```pycon
>>> from whenever import Weekday, MONDAY, SUNDAY
>>> MONDAY is Weekday.MONDAY
True
```
[`Date`](reference/date.md#whenever.Date) and other date-carrying types return
`Weekday` from their [`day_of_week()`](reference/date.md#whenever.Date.day_of_week) method:
```pycon
>>> Date(2024, 12, 25).day_of_week()
Weekday.WEDNESDAY
```
### *type* whenever.RoundModeStr *= [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['ceil', 'expand', 'floor', 'trunc', 'half_ceil', 'half_expand', 'half_floor', 'half_trunc', 'half_even']*
See [Modes](guide/rounding.md#rounding-modes) for more information.
### *type* whenever.DisambiguateStr *= [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['compatible', 'earlier', 'later', 'raise']*
See [Ambiguity in timezones](guide/ambiguity.md#ambiguity) for more information.
### *type* whenever.DeltaUnitStr *= [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'nanoseconds']*
### *type* whenever.DateDeltaUnitStr *= [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['years', 'months', 'weeks', 'days']*
### *type* whenever.ExactDeltaUnitStr *= [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['weeks', 'days', 'hours', 'minutes', 'seconds', 'nanoseconds']*
### *type* whenever.OffsetMismatchStr *= [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['raise', 'keep_instant', 'keep_local']*
# reference/partial-types.md
# Partial types
This section describes the “smaller” date & time types provided by
`whenever`: [`Date`](reference/date.md#whenever.Date), [`Time`](reference/time.md#whenever.Time), [`YearMonth`](reference/yearmonth.md#whenever.YearMonth), [`MonthDay`](reference/monthday.md#whenever.MonthDay),
and [`IsoWeekDate`](reference/isoweekdate.md#whenever.IsoWeekDate).
## Overview
| Type | Represents | Example |
|-----------------------------------------------------------------------------------|---------------------------------------------|----------------------------------------|
| [`Date`](reference/date.md#whenever.Date) | A calendar date (year, month, day) | `Date(2024, 3, 15)` |
| [`Time`](reference/time.md#whenever.Time) | A time of day (hour, minute, second…) | `Time(14, 30)` |
| [`YearMonth`](reference/yearmonth.md#whenever.YearMonth) | A year and month without a day | `YearMonth(2024, 3)` |
| [`MonthDay`](reference/monthday.md#whenever.MonthDay) | A month and day without a year | `MonthDay(3, 15)` |
| [`IsoWeekDate`](reference/isoweekdate.md#whenever.IsoWeekDate) | An ISO 8601 week date (year, week, weekday) | `IsoWeekDate(2024, 1, Weekday.MONDAY)` |
## Date
[`Date`](reference/date.md#whenever.Date) represents a calendar date. It supports arithmetic with
[`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) and calculating the difference between two dates:
```python
>>> d = Date(2023, 1, 31)
>>> d.add(months=1) # End-of-month pinning
Date("2023-02-28")
>>> d.since(Date(2022, 10, 15), in_units=["months", "days"])
ItemizedDateDelta("P3m16d")
```
You can combine a [`Date`](reference/date.md#whenever.Date) with a [`Time`](reference/time.md#whenever.Time) to get a [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime):
```python
>>> Date(2023, 6, 15).at(Time(9, 0))
PlainDateTime("2023-06-15T09:00:00")
```
## Time
[`Time`](reference/time.md#whenever.Time) represents a time of day, independent of any date or timezone.
Sub-second precision is supported down to nanoseconds.
```python
>>> Time(14, 30, nanosecond=500_000_000)
Time("14:30:00.5")
```
## YearMonth and MonthDay
[`YearMonth`](reference/yearmonth.md#whenever.YearMonth) and [`MonthDay`](reference/monthday.md#whenever.MonthDay) are useful for recurring events or
partial date specifications (e.g. a birthday or an annual deadline):
```python
>>> YearMonth(2024, 3).on_day(22)
Date("2024-03-22")
>>> MonthDay(2, 29).in_year(2024)
Date("2024-02-29")
```
# reference/pattern-format.md
# Pattern format
Custom format and parse patterns allow you to format datetime values into
strings and parse strings into datetime values, using a pattern string
that describes the expected format.
## Quick example
```python
>>> from whenever import Date, Time, OffsetDateTime, hours
>>> Date(2024, 3, 15).format("YYYY/MM/DD")
'2024/03/15'
>>> Date.parse("2024/03/15", format="YYYY/MM/DD")
Date("2024-03-15")
>>> OffsetDateTime(2024, 3, 15, 14, 30, offset=+2).format(
... "EEE, DD MMM YYYY hh:mm:ssxxx"
... )
'Fri, 15 Mar 2024 14:30:00+02:00'
```
## Specifiers
Each pattern is a string containing specifiers and literal text.
Specifiers are sequences of the same letter that are replaced by
the corresponding value.
### Date specifiers
| Symbol | Meaning | Pattern | Example output |
|----------|----------------------------------|-----------------------------------------|------------------------------------------|
| `Y` | year | `YY` [1](#id8) `YYYY` | `24` `2024` |
| `M` | month | `M` `MM` `MMM` `MMMM` | `3` `03` `Mar` `March` |
| `D` | day of month | `D` `DD` | `5` `05` |
| `E` | day of week [2](#id9) | `EEE` `EEEE` | `Fri` `Friday` |
### Time specifiers
| Symbol | Meaning | Pattern | Example output |
|----------|---------------------------------------------------|----------------------------------------------|--------------------------------------------------------------------------------|
| `h` | hour | `h` `hh` | `4` `04` |
| `i` | hour (12-hour) | `i` `ii` | `4` `04` |
| `m` | minute | `m` `mm` | `5` `05` |
| `s` | second | `s` `ss` | `5` `05` |
| `S` | second, optional [3](#id10) | `SS` | `05`, (omitted) |
| `f` | fractional seconds, exact digits | `f` `ff` `fff` … `fffffffff` | `1` `12`, `00` `123`, `400` … `123456789`, `374930000` |
| `F` | fractional seconds, trimmed [4](#id11) | `F` `FF` `FFF` … `FFFFFFFFF` | `1` `12`, (omitted) `123`, `4` … `123456789`, `37493` |
| `a` | AM/PM [5](#id12) | `a` `aa` | `P` `PM` |
### Offset and timezone specifiers
See [Timezones](fundamentals/timezones.md#timezones-explained) for background on timezones, offsets, and abbreviations.
| Symbol | Meaning | Pattern | Example output |
|----------|----------------------------------------------------|-------------------------------------------------------|---------------------------------------------------------------------------------------|
| `x` | Offset hours and minutes | `x` `xx` `xxx` `xxxx` `xxxxx` | `+02` `+0230` `+02:30` `+023045` `+02:30:45` |
| `X` | Offset hours and minutes, with `Z` for zero offset | `X` `XX` `XXX` `XXXX` `XXXXX` | `+02` `+0230` `+02:30` `+023045` `+02:30:45` or `Z` when zero |
| `V` | IANA timezone ID | `VV` | `Europe/Paris` |
| `z` | Timezone abbreviation [6](#id13) | `zz` | `CET`, `CEST` |
```python
>>> ZonedDateTime(2024, 7, 15, 14, 30, tz="Europe/Paris").format(
... "YYYY-MM-DD hh:mm zz"
... )
'2024-07-15 14:30 CEST'
>>> ZonedDateTime.parse(
... "2024-07-15 14:30+02:00[Europe/Paris]",
... format="YYYY-MM-DD hh:mmxxx'['VV']'",
... )
ZonedDateTime("2024-07-15 14:30:00+02:00[Europe/Paris]")
```
### Supported specifiers per type
| Type | Date | Time | `x`/`X` | `VV`/`zz` |
|-----------------------------------------------------------------------------------------|--------|--------|-----------|-------------|
| [`Date`](reference/date.md#whenever.Date) | ✅ | ❌ | ❌ | ❌ |
| [`Time`](reference/time.md#whenever.Time) | ❌ | ✅ | ❌ | ❌ |
| [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) | ✅ | ✅ | ❌ | ❌ |
| [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) | ✅ | ✅ | ✅ | ❌ |
| [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) | ✅ | ✅ | ✅ | ✅ |
| [`Instant`](reference/instant.md#whenever.Instant) | ✅ | ✅ | ✅ | ❌ |
## Literal text
Common non-letter characters (`:`, `-`, `/`, `.`, `,`, `;`,
`_`, `(`, `)`, digits, spaces, and other ASCII
punctuation) are treated as literals by default:
```python
>>> Date(2024, 3, 15).format("YYYY/MM/DD")
'2024/03/15'
```
**Letters must be quoted** with single quotes to be used as literals.
This prevents accidental use of reserved characters and keeps options
open for future specifiers:
```python
>>> Date(2024, 3, 15).format("YYYY'xx'MM")
'2024xx03'
```
To include a literal single quote, use `''`:
```python
>>> Date(2024, 3, 15).format("YYYY''MM")
"2024'03"
```
### Restrictions
- **ASCII-only**: Pattern strings must contain only ASCII characters.
Non-ASCII characters raise `ValueError`.
- **Reserved characters**: `<`, `>`, `[`, `]`, `{`, `}`, and `#`
are reserved for future use and cannot appear unquoted.
- **No duplicate fields**: A pattern cannot contain two specifiers that
set the same value. For example, `MM` and `MMM` both set the month,
so `"DD MM MMM YYYY"` is invalid.
## Parsing requirements
Some types require specific fields in the parse pattern:
- [`OffsetDateTime.parse()`](reference/offset_datetime.md#whenever.OffsetDateTime.parse) requires an offset (`x`/`X`)
- [`ZonedDateTime.parse()`](reference/zoned_datetime.md#whenever.ZonedDateTime.parse) requires `VV` (timezone ID).
An offset (`x`/`X`) is optional but recommended for DST disambiguation.
- [`Instant.parse()`](reference/instant.md#whenever.Instant.parse) requires an offset (`x`/`X`)
All types that include date fields require `YYYY`, `MM`, and `DD`.
A second value of `60` (leap second) is accepted and normalized to `59`.
See [Are leap seconds supported?](faq.md#faq-leap-seconds) for details.
## Comparison with strftime
The [`parse_strptime()`](reference/offset_datetime.md#whenever.OffsetDateTime.parse_strptime) methods on [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime) and
[`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) are deprecated in favor of
[`parse()`](reference/offset_datetime.md#whenever.OffsetDateTime.parse). Here’s a migration guide:
| strftime | Pattern | Notes |
|------------|-----------|--------------------------------------------------------------------------------------------------------------------------------|
| `%Y` | `YYYY` | |
| `%y` | `YY` | Format only |
| `%m` | `MM` | |
| `%b` | `MMM` | |
| `%B` | `MMMM` | |
| `%d` | `DD` | |
| `%a` | `EEE` | |
| `%A` | `EEEE` | |
| `%H` | `hh` | Note: `hh` = 24-hour |
| `%I` | `ii` | Note: `ii` = 12-hour |
| `%M` | `mm` | |
| `%S` | `ss` | |
| `%f` | `ffffff` | microseconds (6 digits) |
| `%p` | `aa` | |
| `%z` | `xxxx` | `XXXX` for Z-style |
| `%:z` | `xxxxx` | `XXXXX` for Z-style |
| `%Z` | — | Abbreviations are not supported for parsing. See [Timezones](fundamentals/timezones.md#timezones-explained). |
---
* **[1]** `YY` is only supported for formatting. When parsing, use `YYYY` to avoid ambiguity.
* **[2]** During parsing, weekday names are validated against the parsed date. A mismatch raises `ValueError`.
* **[3]** Omitted when both seconds and nanoseconds are zero.
* **[4]** Omitted when the value is zero, with preceding `.` also omitted.
* **[5]** AM/PM is determined by the hour value. Using `i`/`ii` without `a`/`aa` emits a warning about ambiguity.
* **[6]** Timezone abbreviations are ambiguous and not supported for parsing. Use `VV` (IANA timezone ID) instead. See [Timezones](fundamentals/timezones.md#timezones-explained) for details.
# reference/plain_datetime.md
# `PlainDateTime`
### *class* whenever.PlainDateTime(iso_string: [str](https://docs.python.org/3/library/stdtypes.html#str),)
### *class* whenever.PlainDateTime(py_dt: [datetime](https://docs.python.org/3/library/datetime.html#datetime.datetime),)
### *class* whenever.PlainDateTime(year: [int](https://docs.python.org/3/library/functions.html#int), month: [int](https://docs.python.org/3/library/functions.html#int), day: [int](https://docs.python.org/3/library/functions.html#int), hour: [int](https://docs.python.org/3/library/functions.html#int) = 0, minute: [int](https://docs.python.org/3/library/functions.html#int) = 0, second: [int](https://docs.python.org/3/library/functions.html#int) = 0, , nanosecond: [int](https://docs.python.org/3/library/functions.html#int) = 0)
A date and time-of-day without any timezone information.
Represents “wall clock” time as people observe it locally.
It can’t be mixed with exact-time types (e.g. `Instant`,
`ZonedDateTime`) without explicitly assuming a timezone or offset.
```pycon
>>> PlainDateTime(2024, 3, 10, 15, 30)
PlainDateTime("2024-03-10 15:30:00")
```
Can also be constructed from an ISO 8601 string
or a standard library [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime):
```pycon
>>> PlainDateTime("2024-03-10T15:30:00")
PlainDateTime("2024-03-10 15:30:00")
```
Convert to an exact time type by supplying a timezone or offset:
```pycon
>>> dt = PlainDateTime(2024, 3, 10, 15, 30)
>>> dt.assume_tz("Europe/Amsterdam")
ZonedDateTime("2024-03-10 15:30:00+01:00[Europe/Amsterdam]")
>>> dt.assume_fixed_offset(5)
OffsetDateTime("2024-03-10 15:30:00+05:00")
```
When to use this type:
- You need to express a date and time as it would appear on a
wall clock, independent of timezone.
- You receive a datetime without timezone information and need
to represent this lack of information in the type system.
- You’re working in a context where timezones and DST
transitions truly don’t apply (e.g. a simulation).
#### *classmethod* from_py_datetime(d: [datetime](https://docs.python.org/3/library/datetime.html#datetime.datetime),) → \_T
Create an instance from a [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime) object.
#### Deprecated
Deprecated since version 0.10.0: Use the constructor instead (e.g. `Instant(d)`,
`ZonedDateTime(d)`, etc.)
#### 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`](reference/zoned_datetime.md#whenever.ZonedDateTime)
requires a [`ZoneInfo`](https://docs.python.org/3/library/zoneinfo.html#zoneinfo.ZoneInfo) tzinfo.
#### WARNING
No exceptions are raised if the datetime is ambiguous.
Its `fold` attribute is used to disambiguate.
#### *classmethod* parse(s: [str](https://docs.python.org/3/library/stdtypes.html#str), , , format: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)
Parse a plain datetime from a custom pattern string.
See [Pattern format](reference/pattern-format.md#pattern-format) for details.
```pycon
>>> PlainDateTime.parse("2024-03-15 14:30", format="YYYY-MM-DD hh:mm")
PlainDateTime("2024-03-15 14:30:00")
```
#### *classmethod* parse_iso(s: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)
Parse the popular ISO format `YYYY-MM-DDTHH:MM:SS`
The inverse of the `format_iso()` method.
```pycon
>>> PlainDateTime.parse_iso("2020-08-15T23:12:00")
PlainDateTime("2020-08-15 23:12:00")
```
#### *classmethod* parse_strptime(s: [str](https://docs.python.org/3/library/stdtypes.html#str), , , format: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)
Parse a plain datetime using the standard library `strptime()` method.
#### Deprecated
Deprecated since version 0.10.0: Use [`parse()`](reference/plain_datetime.md#whenever.PlainDateTime.parse) with a pattern string instead, or use
`PlainDateTime(datetime.strptime(...))`.
#### \_\_add_\_(delta: [DateDelta](reference/deprecated.md#whenever.DateDelta) | [TimeDelta](reference/time_delta.md#whenever.TimeDelta)) → [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)
Add a delta to this datetime.
#### WARNING
Adding exact time units (a `TimeDelta`) to a `PlainDateTime` does
not account for timezone transitions that may occur in the interval.
Use `.assume_tz('') + delta` if you know the timezone.
Use `.add(..., naive_arithmetic_ok=True)` or Python’s
standard warning filters to suppress.
#### \_\_eq_\_(other: [object](https://docs.python.org/3/library/functions.html#object)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare objects for equality.
Only ever equal to other [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) instances with the
same values.
#### WARNING
To comply with the Python data model, this method can’t
raise a [`TypeError`](https://docs.python.org/3/library/exceptions.html#TypeError) when comparing with other types.
Although it seems to be the sensible response, it would result in
[surprising behavior](https://stackoverflow.com/a/33417512)
when using values as dictionary keys.
Use mypy’s `--strict-equality` flag to detect and prevent this.
```pycon
>>> 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.
```
#### \_\_format_\_(spec: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [str](https://docs.python.org/3/library/stdtypes.html#str)
Default object formatter.
Return str(self) if format_spec is empty. Raise TypeError otherwise.
#### \_\_ge_\_(other: [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>=value.
#### \_\_gt_\_(other: [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>value.
#### \_\_le_\_(other: [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self<=value.
#### \_\_lt_\_(other: [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self') + delta` if you know the timezone.
Pass `naive_arithmetic_ok=True` to suppress;
Python’s standard warning filters also apply.
#### assume_fixed_offset(offset: [int](https://docs.python.org/3/library/functions.html#int) | [TimeDelta](reference/time_delta.md#whenever.TimeDelta),) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
Assume the datetime has the given offset, creating an `OffsetDateTime`.
```pycon
>>> PlainDateTime(2020, 8, 15, 23, 12).assume_fixed_offset(+2)
OffsetDateTime("2020-08-15 23:12:00+02:00")
```
#### assume_system_tz(disambiguate: [DisambiguateStr](reference/other-types.md#whenever.DisambiguateStr) = 'compatible') → [ZonedDateTime](reference/zoned_datetime.md#whenever.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](https://whenever.rtfd.io/en/latest/guide/ambiguity.html)
for more information.
```pycon
>>> 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]")
```
#### assume_tz(tz: [str](https://docs.python.org/3/library/stdtypes.html#str), , disambiguate: [DisambiguateStr](reference/other-types.md#whenever.DisambiguateStr) = 'compatible') → [ZonedDateTime](reference/zoned_datetime.md#whenever.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](https://whenever.rtfd.io/en/latest/guide/ambiguity.html)
for more information.
```pycon
>>> 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_utc() → [Instant](reference/instant.md#whenever.Instant)
Assume the datetime is in UTC, creating an `Instant`.
```pycon
>>> PlainDateTime(2020, 8, 15, 23, 12).assume_utc()
Instant("2020-08-15 23:12:00Z")
```
#### date() → [Date](reference/date.md#whenever.Date)
The date part of the datetime
```pycon
>>> d = PlaineDateTime("2020-01-02 03:04:05")
>>> d.date()
Date("2021-01-02")
```
To perform the inverse, use [`Date.at()`](reference/date.md#whenever.Date.at) and a method
like [`assume_utc()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_utc) or
[`assume_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_tz):
```pycon
>>> date.at(time).assume_tz("Europe/London")
ZonedDateTime("2021-01-02T03:04:05+00:00[Europe/London]")
```
#### day_of_year() → [int](https://docs.python.org/3/library/functions.html#int)
Ordinal day in the year (1–366)
```pycon
>>> PlainDateTime(2021, 1, 2).day_of_year()
2
```
#### days_in_month() → [int](https://docs.python.org/3/library/functions.html#int)
Number of days in the current month (28–31)
```pycon
>>> PlainDateTime(2024, 2, 1).days_in_month()
29
```
#### days_in_year() → [int](https://docs.python.org/3/library/functions.html#int)
Number of days in the current year (365 or 366)
```pycon
>>> PlainDateTime(2024, 1, 1).days_in_year()
366
```
#### difference(other: [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime), , , ignore_dst: [bool](https://docs.python.org/3/library/functions.html#bool) = ..., naive_arithmetic_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
Calculate the exact time difference between two plain datetimes.
This method returns the exact elapsed [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) between two
`PlainDateTime` values. Equivalent to the subtraction operator (`-`),
but allows suppressing the [`NaiveArithmeticWarning`](reference/exceptions.md#whenever.NaiveArithmeticWarning)
via the `naive_arithmetic_ok` parameter.
Use [`since()`](reference/plain_datetime.md#whenever.PlainDateTime.since) or [`until()`](reference/plain_datetime.md#whenever.PlainDateTime.until) for more advanced options such as
calendar units, unit decomposition, and rounding.
#### WARNING
Calculating the difference between two `PlainDateTime` values does
not account for timezone transitions. Use [`assume_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_tz) to convert
to a `ZonedDateTime` first for accurate results.
#### end_of(unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['year', 'month', 'week_mon', 'week_sun', 'day', 'hour', 'minute', 'second'],) → [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)
The end of the given unit
```pycon
>>> PlainDateTime(2024, 8, 15, 14, 30, 45).end_of("day")
PlainDateTime("2024-08-15 23:59:59.999999999")
>>> PlainDateTime(2024, 8, 15, 14, 30, 45).end_of("hour")
PlainDateTime("2024-08-15 14:59:59.999999999")
```
See also [`start_of()`](reference/plain_datetime.md#whenever.PlainDateTime.start_of)
#### format(pattern: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [str](https://docs.python.org/3/library/stdtypes.html#str)
Format as a custom pattern string.
Also available via `f"{dt:YYYY-MM-DD hh:mm}"` (Python’s `__format__`
protocol), where an empty spec falls back to [`__str__()`](reference/plain_datetime.md#whenever.PlainDateTime.__str__).
See [Pattern format](reference/pattern-format.md#pattern-format) for details.
```pycon
>>> PlainDateTime(2024, 3, 15, 14, 30).format("YYYY-MM-DD hh:mm")
'2024-03-15 14:30'
```
#### format_iso(, unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond', 'auto'] = 'auto', basic: [bool](https://docs.python.org/3/library/functions.html#bool) = False, sep: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['T', ' '] = 'T') → [str](https://docs.python.org/3/library/stdtypes.html#str)
Convert to the popular ISO format `YYYY-MM-DDTHH:MM:SS`
The inverse of the `parse_iso()` method.
#### in_leap_year() → [bool](https://docs.python.org/3/library/functions.html#bool)
Whether this date’s year is a leap year
```pycon
>>> PlainDateTime(2024, 1, 1).in_leap_year()
True
```
#### py_datetime() → [datetime](https://docs.python.org/3/library/datetime.html#datetime.datetime)
Convert to a standard library [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime)
#### Deprecated
Deprecated since version 0.10.0: Use [`to_stdlib()`](reference/plain_datetime.md#whenever.PlainDateTime.to_stdlib) instead.
#### replace(, year: [int](https://docs.python.org/3/library/functions.html#int) = ..., month: [int](https://docs.python.org/3/library/functions.html#int) = ..., day: [int](https://docs.python.org/3/library/functions.html#int) = ..., hour: [int](https://docs.python.org/3/library/functions.html#int) = ..., minute: [int](https://docs.python.org/3/library/functions.html#int) = ..., second: [int](https://docs.python.org/3/library/functions.html#int) = ..., nanosecond: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)
Construct a new instance with the given fields replaced.
#### replace_date(d: [Date](reference/date.md#whenever.Date),) → [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)
Construct a new instance with the date replaced.
#### replace_time(t: [Time](reference/time.md#whenever.Time),) → [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)
Construct a new instance with the time replaced.
#### round(unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['day', 'hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'] | [TimeDelta](reference/time_delta.md#whenever.TimeDelta) = 'second', , , increment: [int](https://docs.python.org/3/library/functions.html#int) = 1, mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = 'half_even') → [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)
Round the datetime to the specified unit and increment,
or to a multiple of a [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta).
Different rounding modes are available.
```pycon
>>> d = PlainDateTime(2020, 8, 15, 23, 24, 18)
>>> d.round("day")
PlainDateTime("2020-08-16 00:00:00")
>>> d.round("minute", increment=15, mode="floor")
PlainDateTime("2020-08-15 23:15:00")
```
#### since(b: [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime), , , total: [DeltaUnitStr](reference/other-types.md#whenever.DeltaUnitStr), naive_arithmetic_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [float](https://docs.python.org/3/library/functions.html#float)
#### since(b: [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime), , , in_units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DeltaUnitStr')], round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = ..., round_increment: [int](https://docs.python.org/3/library/functions.html#int) = ..., naive_arithmetic_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
Calculate the duration since another PlainDateTime,
in terms of the specified units.
```pycon
>>> d1 = PlainDateTime(2020, 8, 15, 23, 12)
>>> d2 = PlainDateTime(2020, 8, 14, 22)
>>> d1.since(d2, in_units=["hours", "minutes"],
... round_increment=15,
... round_mode="ceil")
ItemizedDelta("PT25h15m")
```
#### start_of(unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['year', 'month', 'week_mon', 'week_sun', 'day', 'hour', 'minute', 'second'],) → [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)
The start of the given unit
```pycon
>>> PlainDateTime(2024, 8, 15, 14, 30, 45).start_of("day")
PlainDateTime("2024-08-15 00:00:00")
>>> PlainDateTime(2024, 8, 15, 14, 30, 45).start_of("hour")
PlainDateTime("2024-08-15 14:00:00")
```
#### subtract(d: [DateTimeDelta](reference/deprecated.md#whenever.DateTimeDelta) | [TimeDelta](reference/time_delta.md#whenever.TimeDelta) | [DateDelta](reference/deprecated.md#whenever.DateDelta) | [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta) | [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta), , , ignore_dst: [bool](https://docs.python.org/3/library/functions.html#bool) = ..., naive_arithmetic_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)
#### subtract(, years: [int](https://docs.python.org/3/library/functions.html#int) = ..., months: [int](https://docs.python.org/3/library/functions.html#int) = ..., weeks: [int](https://docs.python.org/3/library/functions.html#int) = ..., days: [int](https://docs.python.org/3/library/functions.html#int) = ..., hours: [float](https://docs.python.org/3/library/functions.html#float) = ..., minutes: [float](https://docs.python.org/3/library/functions.html#float) = ..., seconds: [float](https://docs.python.org/3/library/functions.html#float) = ..., milliseconds: [float](https://docs.python.org/3/library/functions.html#float) = ..., microseconds: [float](https://docs.python.org/3/library/functions.html#float) = ..., nanoseconds: [int](https://docs.python.org/3/library/functions.html#int) = ..., ignore_dst: [bool](https://docs.python.org/3/library/functions.html#bool) = ..., naive_arithmetic_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)
Subtract a time amount from this datetime.
See [`add()`](reference/plain_datetime.md#whenever.PlainDateTime.add) for more information.
#### time() → [Time](reference/time.md#whenever.Time)
The time-of-day part of the datetime
```pycon
>>> d = ZonedDateTime("2021-01-02T03:04:05+01:00[Europe/Paris])"
>>> d.time()
Time(03:04:05)
```
To perform the inverse, use [`Time.on()`](reference/time.md#whenever.Time.on) and a method
like [`assume_utc()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_utc) or
[`assume_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_tz):
```pycon
>>> time.on(date).assume_tz("Europe/Paris")
ZonedDateTime("2021-01-02T03:04:05+01:00[Europe/Paris]")
```
#### to_stdlib() → [datetime](https://docs.python.org/3/library/datetime.html#datetime.datetime)
Convert to a standard library [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime)
#### NOTE
- Nanoseconds are truncated to microseconds.
If you wish to customize the rounding behavior, use
the `round()` method first.
- For [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) linked to a system timezone without a
IANA timezone ID, the returned Python datetime will have
a fixed offset ([`timezone`](https://docs.python.org/3/library/datetime.html#datetime.timezone) tzinfo)
#### until(b: [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime), , , total: [DeltaUnitStr](reference/other-types.md#whenever.DeltaUnitStr), naive_arithmetic_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [float](https://docs.python.org/3/library/functions.html#float)
#### until(b: [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime), , , in_units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DeltaUnitStr')], round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = ..., round_increment: [int](https://docs.python.org/3/library/functions.html#int) = ..., naive_arithmetic_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
Inverse of the `since()` method. See [`since()`](reference/plain_datetime.md#whenever.PlainDateTime.since) for more information.
#### MAX *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)]* *= PlainDateTime("9999-12-31 23:59:59.999999999")*
The maximum representable value of this type.
#### MIN *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)]* *= PlainDateTime("0001-01-01 00:00:00")*
The minimum representable value of this type.
#### *property* day *: [int](https://docs.python.org/3/library/functions.html#int)*
The day component of the datetime
#### *property* hour *: [int](https://docs.python.org/3/library/functions.html#int)*
The hour component of the datetime
#### *property* minute *: [int](https://docs.python.org/3/library/functions.html#int)*
The minute component of the datetime
#### *property* month *: [int](https://docs.python.org/3/library/functions.html#int)*
The month component of the datetime
#### *property* nanosecond *: [int](https://docs.python.org/3/library/functions.html#int)*
The nanosecond component of the datetime
#### *property* second *: [int](https://docs.python.org/3/library/functions.html#int)*
The second component of the datetime
#### *property* year *: [int](https://docs.python.org/3/library/functions.html#int)*
The year component of the datetime
# reference/time.md
# `Time`
### *class* whenever.Time(iso_string: [str](https://docs.python.org/3/library/stdtypes.html#str),)
### *class* whenever.Time(t: [time](https://docs.python.org/3/library/datetime.html#datetime.time),)
### *class* whenever.Time(hour: [int](https://docs.python.org/3/library/functions.html#int) = 0, minute: [int](https://docs.python.org/3/library/functions.html#int) = 0, second: [int](https://docs.python.org/3/library/functions.html#int) = 0, , nanosecond: [int](https://docs.python.org/3/library/functions.html#int) = 0)
Time of day without a date component.
```pycon
>>> t = Time(12, 30, 0)
Time("12:30:00")
```
Can also be constructed from an ISO 8601 string:
```pycon
>>> Time("12:30:00")
Time("12:30:00")
```
Or a standard library [`time`](https://docs.python.org/3/library/datetime.html#datetime.time):
```pycon
>>> Time(time(12, 30, 0))
Time("12:30:00")
```
#### NOTE
When constructing from a [`time`](https://docs.python.org/3/library/datetime.html#datetime.time), the `fold`
attribute and `tzinfo` are ignored.
Sub-second precision up to nanoseconds is supported:
```pycon
>>> Time(12, 30, 0, nanosecond=1)
Time("12:30:00.000000001")
```
Times can be compared and sorted:
```pycon
>>> Time(12, 30) > Time(8, 0)
True
```
#### *classmethod* from_py_time(t: [time](https://docs.python.org/3/library/datetime.html#datetime.time),) → [Time](reference/time.md#whenever.Time)
Create from a [`time`](https://docs.python.org/3/library/datetime.html#datetime.time)
```pycon
>>> Time.from_py_time(time(12, 30, 0))
Time(12:30:00)
```
#### Deprecated
Deprecated since version 0.10.0: Use the constructor `Time(t)` instead.
#### *classmethod* parse(s: [str](https://docs.python.org/3/library/stdtypes.html#str), , , format: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [Time](reference/time.md#whenever.Time)
Parse a time from a custom pattern string.
See [Pattern format](reference/pattern-format.md#pattern-format) for details.
```pycon
>>> Time.parse("14:30:05", format="hh:mm:ss")
Time(14:30:05)
>>> Time.parse("02:30 PM", format="ii:mm aa")
Time(14:30:00)
```
#### *classmethod* parse_iso(s: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [Time](reference/time.md#whenever.Time)
Create from the ISO 8601 time format
Inverse of [`format_iso()`](reference/time.md#whenever.Time.format_iso)
```pycon
>>> Time.parse_iso("12:30:00")
Time(12:30:00)
```
#### \_\_eq_\_(other: [object](https://docs.python.org/3/library/functions.html#object)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare for equality
```pycon
>>> t = Time(12, 30, 0)
>>> t == Time(12, 30, 0)
True
>>> t == Time(12, 30, 1)
False
```
#### \_\_ge_\_(other: [Time](reference/time.md#whenever.Time)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>=value.
#### \_\_gt_\_(other: [Time](reference/time.md#whenever.Time)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>value.
#### \_\_le_\_(other: [Time](reference/time.md#whenever.Time)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self<=value.
#### \_\_lt_\_(other: [Time](reference/time.md#whenever.Time)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>> Time(14, 30, 5).format("hh:mm:ss")
'14:30:05'
>>> Time(14, 30).format("ii:mm aa")
'02:30 PM'
```
#### format_iso(, unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond', 'auto'] = 'auto', basic: [bool](https://docs.python.org/3/library/functions.html#bool) = False) → [str](https://docs.python.org/3/library/stdtypes.html#str)
Format as the ISO 8601 time format.
Inverse of [`parse_iso()`](reference/time.md#whenever.Time.parse_iso).
```pycon
>>> 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'
```
#### on(d: [Date](reference/date.md#whenever.Date),) → [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)
Combine a time with a date to create a datetime
```pycon
>>> t = Time(12, 30)
>>> t.on(Date(2021, 1, 2))
PlainDateTime("2021-01-02 12:30:00")
```
Then, use methods like [`assume_utc()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_utc)
or [`assume_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_tz)
to find the corresponding exact time:
```pycon
>>> t.on(Date(2021, 1, 2)).assume_tz("America/New_York")
ExactDateTime("2021-01-02 12:30:00-05:00[America/New_York]")
```
#### py_time() → [time](https://docs.python.org/3/library/datetime.html#datetime.time)
Convert to a standard library [`time`](https://docs.python.org/3/library/datetime.html#datetime.time)
#### Deprecated
Deprecated since version 0.10.0: Use [`to_stdlib()`](reference/time.md#whenever.Time.to_stdlib) instead.
#### replace(hour: [int](https://docs.python.org/3/library/functions.html#int) = ..., minute: [int](https://docs.python.org/3/library/functions.html#int) = ..., second: [int](https://docs.python.org/3/library/functions.html#int) = ..., nanosecond: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [Time](reference/time.md#whenever.Time)
Create a new instance with the given fields replaced
```pycon
>>> t = Time(12, 30, 0)
>>> d.replace(minute=3, nanosecond=4_000)
Time(12:03:00.000004)
```
#### round(unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'] | [TimeDelta](reference/time_delta.md#whenever.TimeDelta) = 'second', , , increment: [int](https://docs.python.org/3/library/functions.html#int) = 1, mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = 'half_even') → [Time](reference/time.md#whenever.Time)
Round the time to the specified unit and increment,
or to a multiple of a [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta).
Various rounding modes are available.
```pycon
>>> Time(12, 39, 59).round("minute", 15)
Time(12:45:00)
>>> Time(8, 9, 13).round("second", 5, mode="floor")
Time(08:09:10)
>>> Time(12, 39, 59).round(TimeDelta(minutes=15))
Time(12:45:00)
```
#### to_stdlib() → [time](https://docs.python.org/3/library/datetime.html#datetime.time)
Convert to a standard library [`time`](https://docs.python.org/3/library/datetime.html#datetime.time)
#### NOTE
Nanoseconds are truncated to microseconds.
If you need more control over rounding, use [`round()`](reference/time.md#whenever.Time.round) first.
#### MAX *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[Time](reference/time.md#whenever.Time)]* *= Time("23:59:59.999999999")*
The maximum time, just before midnight
#### MIDNIGHT *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[Time](reference/time.md#whenever.Time)]* *= Time("00:00:00")*
Alias for [`MIN`](reference/time.md#whenever.Time.MIN)
#### MIN *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[Time](reference/time.md#whenever.Time)]* *= Time("00:00:00")*
The minimum time, at midnight
#### NOON *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[Time](reference/time.md#whenever.Time)]* *= Time("12:00:00")*
The time at noon
#### *property* hour *: [int](https://docs.python.org/3/library/functions.html#int)*
The hour component of the time
```pycon
>>> Time(12, 30, 0).hour
12
```
#### *property* minute *: [int](https://docs.python.org/3/library/functions.html#int)*
The minute component of the time
```pycon
>>> Time(12, 30, 0).minute
30
```
#### *property* nanosecond *: [int](https://docs.python.org/3/library/functions.html#int)*
The nanosecond component of the time
```pycon
>>> Time("12:30:00.003).nanosecond
3000000
```
#### *property* second *: [int](https://docs.python.org/3/library/functions.html#int)*
The second component of the time
>>> Time(12, 30, 0).second
0
# reference/time_delta.md
# `TimeDelta`
### *class* whenever.TimeDelta(iso_string: [str](https://docs.python.org/3/library/stdtypes.html#str),)
### *class* whenever.TimeDelta(py_timedelta: [timedelta](https://docs.python.org/3/library/datetime.html#datetime.timedelta),)
### *class* whenever.TimeDelta(, weeks: [float](https://docs.python.org/3/library/functions.html#float) = 0, days: [float](https://docs.python.org/3/library/functions.html#float) = 0, hours: [float](https://docs.python.org/3/library/functions.html#float) = 0, minutes: [float](https://docs.python.org/3/library/functions.html#float) = 0, seconds: [float](https://docs.python.org/3/library/functions.html#float) = 0, milliseconds: [float](https://docs.python.org/3/library/functions.html#float) = 0, microseconds: [float](https://docs.python.org/3/library/functions.html#float) = 0, nanoseconds: [int](https://docs.python.org/3/library/functions.html#int) = 0, days_assumed_24h_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = UNSET)
A duration consisting of a precise time: hours, minutes, (nano)seconds.
For durations including months or days, use [`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta),
or [`ItemizedDateDelta`](reference/itemized_date_delta.md#whenever.ItemizedDateDelta) for date-only durations.
The inputs are normalized, so 90 minutes becomes 1 hour and 30 minutes,
for example.
```pycon
>>> d = TimeDelta(hours=1, minutes=90)
TimeDelta("PT2h30m")
>>> d.total("minutes")
150.0
```
Can also be constructed from an ISO 8601 duration string
or a standard library [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta):
```pycon
>>> TimeDelta("PT2h30m")
TimeDelta("PT2h30m")
```
#### NOTE
Subclasses of [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta) are not accepted,
because they often add additional state that cannot be represented.
`TimeDelta` can be added to or subtracted from datetime types
to shift them by an exact amount of time:
```pycon
>>> Instant("2022-10-24 00:00Z") + TimeDelta(hours=3)
Instant("2022-10-24 03:00:00Z")
```
#### NOTE
A shorter way to instantiate a timedelta is to use the helper functions
[`hours()`](reference/time_delta.md#whenever.hours), [`minutes()`](reference/time_delta.md#whenever.minutes), etc.
#### *classmethod* from_py_timedelta(td: [timedelta](https://docs.python.org/3/library/datetime.html#datetime.timedelta),) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
Create from a [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta)
```pycon
>>> TimeDelta.from_py_timedelta(timedelta(seconds=5400))
TimeDelta("PT1h30m")
```
#### Deprecated
Deprecated since version 0.10.0: Use the constructor `TimeDelta(td)` instead.
#### *classmethod* parse_iso(s: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
Parse the *popular interpretation* of the ISO 8601 duration format.
Does not parse all possible ISO 8601 durations.
See [here](reference/deltas.md#iso8601-durations) for more information.
Inverse of [`format_iso()`](reference/time_delta.md#whenever.TimeDelta.format_iso)
```pycon
>>> TimeDelta.parse_iso("PT1H80M")
TimeDelta("PT2h20m")
```
#### NOTE
Any duration with a date part is considered invalid.
`PT0S` is valid, but `P0D` is not.
#### \_\_abs_\_() → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
The absolute value
```pycon
>>> d = TimeDelta(hours=-1, minutes=-30)
>>> abs(d)
TimeDelta("PT1h30m")
```
#### \_\_add_\_(other: [TimeDelta](reference/time_delta.md#whenever.TimeDelta)) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
#### \_\_add_\_(other: [Instant](reference/instant.md#whenever.Instant)) → [Instant](reference/instant.md#whenever.Instant)
#### \_\_add_\_(other: [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)) → [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)
#### \_\_add_\_(other: [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)) → [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime)
#### \_\_add_\_(other: [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Add two deltas together
```pycon
>>> d = TimeDelta(hours=1, minutes=30)
>>> d + TimeDelta(minutes=30)
TimeDelta("PT2h")
```
#### \_\_bool_\_() → [bool](https://docs.python.org/3/library/functions.html#bool)
True if the value is non-zero
```pycon
>>> bool(TimeDelta())
False
>>> bool(TimeDelta(minutes=1))
True
```
#### \_\_eq_\_(other: [object](https://docs.python.org/3/library/functions.html#object)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare for equality
```pycon
>>> d = TimeDelta(hours=1, minutes=30)
>>> d == TimeDelta(minutes=90)
True
>>> d == TimeDelta(hours=2)
False
```
#### \_\_floordiv_\_(other: [TimeDelta](reference/time_delta.md#whenever.TimeDelta)) → [int](https://docs.python.org/3/library/functions.html#int)
Floor division by another delta
```pycon
>>> d = TimeDelta(hours=1, minutes=39)
>>> d // time_delta(minutes=15)
6
```
#### \_\_ge_\_(other: [TimeDelta](reference/time_delta.md#whenever.TimeDelta)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>=value.
#### \_\_gt_\_(other: [TimeDelta](reference/time_delta.md#whenever.TimeDelta)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>value.
#### \_\_le_\_(other: [TimeDelta](reference/time_delta.md#whenever.TimeDelta)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self<=value.
#### \_\_lt_\_(other: [TimeDelta](reference/time_delta.md#whenever.TimeDelta)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>> d = TimeDelta(hours=1, minutes=39)
>>> d % TimeDelta(minutes=15)
TimeDelta("PT9m")
```
#### \_\_mul_\_(other: [float](https://docs.python.org/3/library/functions.html#float)) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
Multiply by a number
```pycon
>>> d = TimeDelta(hours=1, minutes=30)
>>> d * 2.5
TimeDelta("PT3h45m")
```
#### \_\_neg_\_() → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
Negate the value
```pycon
>>> d = TimeDelta(hours=1, minutes=30)
>>> -d
TimeDelta(-PT1h30m)
```
#### \_\_pos_\_() → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
Return the value unchanged
```pycon
>>> d = TimeDelta(hours=1, minutes=30)
>>> +d
TimeDelta("PT1h30m")
```
#### \_\_str_\_() → [str](https://docs.python.org/3/library/stdtypes.html#str)
Format as the *popular interpretation* of the ISO 8601 duration format.
May not strictly adhere to (all versions of) the standard.
See [here](reference/deltas.md#iso8601-durations) for more information.
Inverse of [`parse_iso()`](reference/time_delta.md#whenever.TimeDelta.parse_iso).
```pycon
>>> TimeDelta(hours=1, minutes=30).format_iso()
'PT1H30M'
```
#### \_\_sub_\_(other: [TimeDelta](reference/time_delta.md#whenever.TimeDelta)) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
Subtract two deltas
```pycon
>>> d = TimeDelta(hours=1, minutes=30)
>>> d - TimeDelta(minutes=30)
TimeDelta("PT1h")
```
#### \_\_truediv_\_(other: [float](https://docs.python.org/3/library/functions.html#float)) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
#### \_\_truediv_\_(other: [TimeDelta](reference/time_delta.md#whenever.TimeDelta)) → [float](https://docs.python.org/3/library/functions.html#float)
Divide by a number or another delta
```pycon
>>> 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.
#### add(other: [TimeDelta](reference/time_delta.md#whenever.TimeDelta),) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
#### add(, weeks: [float](https://docs.python.org/3/library/functions.html#float) = ..., days: [float](https://docs.python.org/3/library/functions.html#float) = ..., hours: [float](https://docs.python.org/3/library/functions.html#float) = ..., minutes: [float](https://docs.python.org/3/library/functions.html#float) = ..., seconds: [float](https://docs.python.org/3/library/functions.html#float) = ..., milliseconds: [float](https://docs.python.org/3/library/functions.html#float) = ..., microseconds: [float](https://docs.python.org/3/library/functions.html#float) = ..., nanoseconds: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
Add time to this delta, returning a new delta.
Days and weeks are treated as exact 24-hour and 168-hour units,
which emits a [`DaysAssumed24HoursWarning`](reference/exceptions.md#whenever.DaysAssumed24HoursWarning).
#### format_iso() → [str](https://docs.python.org/3/library/stdtypes.html#str)
Format as the *popular interpretation* of the ISO 8601 duration format.
May not strictly adhere to (all versions of) the standard.
See [here](reference/deltas.md#iso8601-durations) for more information.
Inverse of [`parse_iso()`](reference/time_delta.md#whenever.TimeDelta.parse_iso).
```pycon
>>> TimeDelta(hours=1, minutes=30).format_iso()
'PT1H30M'
```
#### in_days_of_24h() → [float](https://docs.python.org/3/library/functions.html#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.
#### Deprecated
Deprecated since version 0.10.0: Use [`total()`](reference/time_delta.md#whenever.TimeDelta.total) with `'days'` instead.
#### in_hours() → [float](https://docs.python.org/3/library/functions.html#float)
The total size in hours
```pycon
>>> d = TimeDelta(hours=1, minutes=30)
>>> d.in_hours()
1.5
```
#### Deprecated
Deprecated since version 0.10.0: Use [`total()`](reference/time_delta.md#whenever.TimeDelta.total) with `'hours'` instead.
#### in_hrs_mins_secs_nanos() → [tuple](https://docs.python.org/3/library/stdtypes.html#tuple)[[int](https://docs.python.org/3/library/functions.html#int), [int](https://docs.python.org/3/library/functions.html#int), [int](https://docs.python.org/3/library/functions.html#int), [int](https://docs.python.org/3/library/functions.html#int)]
Convert to a tuple of (hours, minutes, seconds, nanoseconds)
```pycon
>>> d = TimeDelta(hours=1, minutes=30, microseconds=5_000_090)
>>> d.in_hrs_mins_secs_nanos()
(1, 30, 5, 90_000)
```
#### Deprecated
Deprecated since version 0.10.0: Use [`in_units()`](reference/time_delta.md#whenever.TimeDelta.in_units) with `['hours', 'minutes', 'seconds', 'nanoseconds']` instead.
#### in_microseconds() → [float](https://docs.python.org/3/library/functions.html#float)
The total size in microseconds
```pycon
>>> d = TimeDelta(seconds=2, nanoseconds=50)
>>> d.in_microseconds()
2_000_000.05
```
#### Deprecated
Deprecated since version 0.10.0: Use [`total()`](reference/time_delta.md#whenever.TimeDelta.total) with `'microseconds'` instead.
#### in_milliseconds() → [float](https://docs.python.org/3/library/functions.html#float)
The total size in milliseconds
```pycon
>>> d = TimeDelta(seconds=2, microseconds=50)
>>> d.in_milliseconds()
2_000.05
```
#### Deprecated
Deprecated since version 0.10.0: Use [`total()`](reference/time_delta.md#whenever.TimeDelta.total) with `'milliseconds'` instead.
#### in_minutes() → [float](https://docs.python.org/3/library/functions.html#float)
The total size in minutes
```pycon
>>> d = TimeDelta(hours=1, minutes=30, seconds=30)
>>> d.in_minutes()
90.5
```
#### Deprecated
Deprecated since version 0.10.0: Use [`total()`](reference/time_delta.md#whenever.TimeDelta.total) with `'minutes'` instead.
#### in_nanoseconds() → [int](https://docs.python.org/3/library/functions.html#int)
The total size in nanoseconds
```pycon
>>> d = TimeDelta(seconds=2, nanoseconds=50)
>>> d.in_nanoseconds()
2_000_000_050
```
#### Deprecated
Deprecated since version 0.10.0: Use [`total()`](reference/time_delta.md#whenever.TimeDelta.total) with `'nanoseconds'` instead.
#### in_seconds() → [float](https://docs.python.org/3/library/functions.html#float)
The total size in seconds
```pycon
>>> d = TimeDelta(minutes=2, seconds=1, microseconds=500_000)
>>> d.in_seconds()
121.5
```
#### Deprecated
Deprecated since version 0.10.0: Use [`total()`](reference/time_delta.md#whenever.TimeDelta.total) with `'seconds'` instead.
#### in_units(units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DeltaUnitStr')], , , round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = 'trunc', round_increment: [int](https://docs.python.org/3/library/functions.html#int) = 1, relative_to: [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime) | [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) = ..., days_assumed_24h_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
Convert to a [`ItemizedDelta`](reference/itemized_delta.md#whenever.ItemizedDelta) with the specified units
```pycon
>>> d = TimeDelta(hours=2, minutes=30, seconds=23, milliseconds=500)
>>> d.in_units(['minutes', 'seconds'])
ItemizedDelta("PT150m24s")
>>> (hrs, mins) = d.in_units(('hours', 'minutes'), round_mode='ceil').values()
(2, 31)
```
* **Parameters:**
* **units** – A sequence of plural unit names, in descending order.
Valid unit names are: `weeks`, `days`, `hours`,
`minutes`, `seconds`, `nanoseconds`.
`years` and `months` are also allowed if `relative_to`
is provided.
* **round_mode** – The rounding mode to use when rounding before conversion.
See [`round()`](reference/time_delta.md#whenever.TimeDelta.round) for details.
* **round_increment** – The rounding increment to use when rounding before conversion.
See [`round()`](reference/time_delta.md#whenever.TimeDelta.round) for details.
* **relative_to** –
A reference datetime required when using calendar units
(`years`, `months`, `days`, or `weeks`) to account for variable unit lengths.
- [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime): DST-aware; emits no warning
- [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime): does not account for time zones; emits
[`NaiveArithmeticWarning`](reference/exceptions.md#whenever.NaiveArithmeticWarning)
- [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime): does not account for DST changes; emits
[`StaleOffsetWarning`](reference/exceptions.md#whenever.StaleOffsetWarning)
#### py_timedelta() → [timedelta](https://docs.python.org/3/library/datetime.html#datetime.timedelta)
Convert to a [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta)
#### Deprecated
Deprecated since version 0.10.0: Use [`to_stdlib()`](reference/time_delta.md#whenever.TimeDelta.to_stdlib) instead.
#### round(unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['week', 'day', 'hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'] | [TimeDelta](reference/time_delta.md#whenever.TimeDelta) = 'second', , , increment: [int](https://docs.python.org/3/library/functions.html#int) = 1, mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = 'half_even', days_assumed_24h_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
Round the delta to the specified unit and increment,
or to a multiple of another [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta).
Various rounding modes are available.
```pycon
>>> t = TimeDelta(seconds=12345)
TimeDelta("PT3h25m45s")
>>> t.round("minute")
TimeDelta("PT3h26m")
>>> t.round("second", increment=10, mode="floor")
TimeDelta("PT3h25m40s")
>>> t.round(TimeDelta(minutes=15))
TimeDelta("PT3h30m")
```
#### subtract(other: [TimeDelta](reference/time_delta.md#whenever.TimeDelta),) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
#### subtract(, weeks: [float](https://docs.python.org/3/library/functions.html#float) = ..., days: [float](https://docs.python.org/3/library/functions.html#float) = ..., hours: [float](https://docs.python.org/3/library/functions.html#float) = ..., minutes: [float](https://docs.python.org/3/library/functions.html#float) = ..., seconds: [float](https://docs.python.org/3/library/functions.html#float) = ..., milliseconds: [float](https://docs.python.org/3/library/functions.html#float) = ..., microseconds: [float](https://docs.python.org/3/library/functions.html#float) = ..., nanoseconds: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
Subtract time from this delta, returning a new delta.
Days and weeks are treated as exact 24-hour and 168-hour units,
which emits a [`DaysAssumed24HoursWarning`](reference/exceptions.md#whenever.DaysAssumed24HoursWarning).
#### to_stdlib() → [timedelta](https://docs.python.org/3/library/datetime.html#datetime.timedelta)
Convert to a [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta)
```pycon
>>> d = TimeDelta(hours=1, minutes=30)
>>> d.to_stdlib()
timedelta(seconds=5400)
```
#### NOTE
Nanoseconds are truncated to microseconds.
If you need more control over rounding, use [`round()`](reference/time_delta.md#whenever.TimeDelta.round) first.
#### total(unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds', 'microseconds', 'nanoseconds'], relative_to: [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime) | [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) = ..., \_warn_stacklevel: [int](https://docs.python.org/3/library/functions.html#int) = 2, days_assumed_24h_ok: [bool](https://docs.python.org/3/library/functions.html#bool) = ...) → [float](https://docs.python.org/3/library/functions.html#float) | [int](https://docs.python.org/3/library/functions.html#int)
The total size in the given unit, as a float (or int for nanoseconds)
For calendar units (years, months, weeks, days), a `relative_to`
argument is required to determine the actual duration of each unit:
- [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime): DST-aware; emits no warning
- [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime): no timezone context; emits
[`NaiveArithmeticWarning`](reference/exceptions.md#whenever.NaiveArithmeticWarning)
- [`OffsetDateTime`](reference/offset_datetime.md#whenever.OffsetDateTime): fixed offset; emits
[`StaleOffsetWarning`](reference/exceptions.md#whenever.StaleOffsetWarning)
```pycon
>>> d = TimeDelta(hours=1, minutes=30)
>>> d.total('minutes')
90.0
```
#### MAX *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[TimeDelta](reference/time_delta.md#whenever.TimeDelta)]* *= TimeDelta("PT87831216h")*
The maximum possible delta
#### MIN *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[TimeDelta](reference/time_delta.md#whenever.TimeDelta)]* *= TimeDelta("-PT87831216h")*
The minimum possible delta
#### ZERO *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[TimeDelta](reference/time_delta.md#whenever.TimeDelta)]* *= TimeDelta("PT0s")*
A delta of zero
### whenever.hours(i: [float](https://docs.python.org/3/library/functions.html#float),) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
Create a [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) with the given number of hours.
`hours(1) == TimeDelta(hours=1)`
### whenever.minutes(i: [float](https://docs.python.org/3/library/functions.html#float),) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
Create a [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) with the given number of minutes.
`minutes(1) == TimeDelta(minutes=1)`
### whenever.seconds(i: [float](https://docs.python.org/3/library/functions.html#float),) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
Create a [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) with the given number of seconds.
`seconds(1) == TimeDelta(seconds=1)`
### whenever.milliseconds(i: [float](https://docs.python.org/3/library/functions.html#float),) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
Create a [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) with the given number of milliseconds.
`milliseconds(1) == TimeDelta(milliseconds=1)`
### whenever.microseconds(i: [float](https://docs.python.org/3/library/functions.html#float),) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
Create a [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) with the given number of microseconds.
`microseconds(1) == TimeDelta(microseconds=1)`
### whenever.nanoseconds(i: [int](https://docs.python.org/3/library/functions.html#int),) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
Create a [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) with the given number of nanoseconds.
`nanoseconds(1) == TimeDelta(nanoseconds=1)`
# reference/yearmonth.md
# `YearMonth`
### *class* whenever.YearMonth(iso_string: [str](https://docs.python.org/3/library/stdtypes.html#str),)
### *class* whenever.YearMonth(year: [int](https://docs.python.org/3/library/functions.html#int), month: [int](https://docs.python.org/3/library/functions.html#int))
A year and month without a day component.
Useful for representing recurring events, billing periods,
or any concept that doesn’t need a specific day.
```pycon
>>> ym = YearMonth(2021, 1)
YearMonth("2021-01")
```
Can also be constructed from an ISO 8601 string:
```pycon
>>> YearMonth("2021-01")
YearMonth("2021-01")
```
#### *classmethod* parse_iso(s: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [YearMonth](reference/yearmonth.md#whenever.YearMonth)
Create from the ISO 8601 format `YYYY-MM` or `YYYYMM`.
Inverse of [`format_iso()`](reference/yearmonth.md#whenever.YearMonth.format_iso)
```pycon
>>> YearMonth.parse_iso("2021-01")
YearMonth("2021-01")
```
#### \_\_eq_\_(other: [object](https://docs.python.org/3/library/functions.html#object)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare for equality
```pycon
>>> ym = YearMonth(2021, 1)
>>> ym == YearMonth(2021, 1)
True
>>> ym == YearMonth(2021, 2)
False
```
#### \_\_ge_\_(other: [YearMonth](reference/yearmonth.md#whenever.YearMonth)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>=value.
#### \_\_gt_\_(other: [YearMonth](reference/yearmonth.md#whenever.YearMonth)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>value.
#### \_\_le_\_(other: [YearMonth](reference/yearmonth.md#whenever.YearMonth)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self<=value.
#### \_\_lt_\_(other: [YearMonth](reference/yearmonth.md#whenever.YearMonth)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Return self>> YearMonth(2021, 1).format_iso()
'2021-01'
```
#### days_in_month() → [int](https://docs.python.org/3/library/functions.html#int)
Number of days in this year-month
```pycon
>>> YearMonth(2024, 2).days_in_month()
29
>>> YearMonth(2023, 2).days_in_month()
28
```
#### days_in_year() → [int](https://docs.python.org/3/library/functions.html#int)
Number of days in this year (365 or 366)
```pycon
>>> YearMonth(2024, 1).days_in_year()
366
```
#### format_iso() → [str](https://docs.python.org/3/library/stdtypes.html#str)
Format as the ISO 8601 year-month format.
Inverse of [`parse_iso()`](reference/yearmonth.md#whenever.YearMonth.parse_iso).
```pycon
>>> YearMonth(2021, 1).format_iso()
'2021-01'
```
#### in_leap_year() → [bool](https://docs.python.org/3/library/functions.html#bool)
Whether this year-month’s year is a leap year
```pycon
>>> YearMonth(2024, 1).in_leap_year()
True
>>> YearMonth(2023, 1).in_leap_year()
False
```
#### on_day(day: [int](https://docs.python.org/3/library/functions.html#int),) → [Date](reference/date.md#whenever.Date)
Create a date from this year-month with a given day
```pycon
>>> YearMonth(2021, 1).on_day(2)
Date("2021-01-02")
```
#### replace(year: [int](https://docs.python.org/3/library/functions.html#int) = ..., month: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [YearMonth](reference/yearmonth.md#whenever.YearMonth)
Create a new instance with the given fields replaced
```pycon
>>> d = YearMonth(2021, 12)
>>> d.replace(month=3)
YearMonth("2021-03")
```
#### MAX *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[YearMonth](reference/yearmonth.md#whenever.YearMonth)]* *= YearMonth("9999-12")*
The maximum possible year-month
#### MIN *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[YearMonth](reference/yearmonth.md#whenever.YearMonth)]* *= YearMonth("0001-01")*
The minimum possible year-month
#### *property* month *: [int](https://docs.python.org/3/library/functions.html#int)*
The month component of the year-month
```pycon
>>> YearMonth(2021, 1).month
1
```
#### *property* year *: [int](https://docs.python.org/3/library/functions.html#int)*
The year component of the year-month
```pycon
>>> YearMonth(2021, 1).year
2021
```
# reference/zoned_datetime.md
# `ZonedDateTime`
### *class* whenever.ZonedDateTime(iso_string: [str](https://docs.python.org/3/library/stdtypes.html#str),)
### *class* whenever.ZonedDateTime(py_dt: [datetime](https://docs.python.org/3/library/datetime.html#datetime.datetime),)
### *class* whenever.ZonedDateTime(year: [int](https://docs.python.org/3/library/functions.html#int), month: [int](https://docs.python.org/3/library/functions.html#int), day: [int](https://docs.python.org/3/library/functions.html#int), hour: [int](https://docs.python.org/3/library/functions.html#int) = 0, minute: [int](https://docs.python.org/3/library/functions.html#int) = 0, second: [int](https://docs.python.org/3/library/functions.html#int) = 0, , nanosecond: [int](https://docs.python.org/3/library/functions.html#int) = 0, tz: [str](https://docs.python.org/3/library/stdtypes.html#str), disambiguate: [DisambiguateStr](reference/other-types.md#whenever.DisambiguateStr) = 'compatible')
A datetime associated with a timezone from the IANA database.
This is the right type when you need both the exact moment *and*
the local date/time at a specific location. Arithmetic is fully
DST-aware: the offset is always kept in sync with the timezone rules.
```pycon
>>> ZonedDateTime("2024-12-08T11[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]")
>>> # From a standard library datetime (must have a ZoneInfo tzinfo)
>>> ZonedDateTime(datetime(2020, 8, 15, 23, 12, tzinfo=ZoneInfo("Europe/London")))
ZonedDateTime("2020-08-15 23:12:00+01:00[Europe/London]")
```
Convert to other types to discard timezone information:
```pycon
>>> d = ZonedDateTime(2024, 7, 1, 12, tz="Europe/Amsterdam")
>>> d.to_instant()
Instant("2024-07-01 10:00:00Z")
>>> d.to_plain()
PlainDateTime("2024-07-01 12:00:00")
```
#### IMPORTANT
To use this type properly, read more about
[ambiguity in timezones](https://whenever.rtfd.io/en/latest/guide/ambiguity.html).
#### *classmethod* from_py_datetime(d: [datetime](https://docs.python.org/3/library/datetime.html#datetime.datetime),) → \_T
Create an instance from a [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime) object.
#### Deprecated
Deprecated since version 0.10.0: Use the constructor instead (e.g. `Instant(d)`,
`ZonedDateTime(d)`, etc.)
#### 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`](reference/zoned_datetime.md#whenever.ZonedDateTime)
requires a [`ZoneInfo`](https://docs.python.org/3/library/zoneinfo.html#zoneinfo.ZoneInfo) tzinfo.
#### WARNING
No exceptions are raised if the datetime is ambiguous.
Its `fold` attribute is used to disambiguate.
#### *classmethod* from_system_tz(year: [int](https://docs.python.org/3/library/functions.html#int), month: [int](https://docs.python.org/3/library/functions.html#int), day: [int](https://docs.python.org/3/library/functions.html#int), hour: [int](https://docs.python.org/3/library/functions.html#int) = 0, minute: [int](https://docs.python.org/3/library/functions.html#int) = 0, second: [int](https://docs.python.org/3/library/functions.html#int) = 0, , nanosecond: [int](https://docs.python.org/3/library/functions.html#int) = 0, disambiguate: [DisambiguateStr](reference/other-types.md#whenever.DisambiguateStr) = 'compatible') → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Create an instance in the system timezone.
Equivalent to `ZonedDateTime(..., tz=)`,
except it also works for system timezones whose corresponding
IANA timezone ID is unknown.
```pycon
>>> ZonedDateTime.from_system_tz(2020, 8, 15, hour=23, minute=12)
ZonedDateTime("2020-08-15 23:12:00+02:00[Europe/Berlin]")
```
#### *classmethod* from_timestamp(i: [int](https://docs.python.org/3/library/functions.html#int) | [float](https://docs.python.org/3/library/functions.html#float), , , tz: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Create an instance from a UNIX timestamp (in seconds).
The inverse of the `timestamp()` method.
#### *classmethod* from_timestamp_millis(i: [int](https://docs.python.org/3/library/functions.html#int), , , tz: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Create an instance from a UNIX timestamp (in milliseconds).
The inverse of the `timestamp_millis()` method.
#### *classmethod* from_timestamp_nanos(i: [int](https://docs.python.org/3/library/functions.html#int), , , tz: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Create an instance from a UNIX timestamp (in nanoseconds).
The inverse of the `timestamp_nanos()` method.
#### *classmethod* now(tz: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Create an instance from the current time in the given timezone.
#### *classmethod* now_in_system_tz() → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Create an instance from the current time in the system timezone.
Equivalent to `Instant.now().to_system_tz()`.
#### *classmethod* parse(s: [str](https://docs.python.org/3/library/stdtypes.html#str), , , format: [str](https://docs.python.org/3/library/stdtypes.html#str), disambiguate: [DisambiguateStr](reference/other-types.md#whenever.DisambiguateStr) = 'compatible') → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Parse a zoned datetime from a custom pattern string.
The pattern **must** include a timezone ID field (`VV`).
An offset field (`x`/`X`) is optional but recommended for
disambiguation during DST transitions.
See [Pattern format](reference/pattern-format.md#pattern-format) for details.
#### TIP
If your input string doesn’t include a timezone ID, parse it with
[`PlainDateTime.parse()`](reference/plain_datetime.md#whenever.PlainDateTime.parse) first, then convert using
[`assume_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_tz).
```pycon
>>> ZonedDateTime.parse(
... "2024-03-15 14:30+01:00[Europe/Paris]",
... format="YYYY-MM-DD hh:mmxxx'['VV']'",
... )
ZonedDateTime("2024-03-15 14:30:00+01:00[Europe/Paris]")
```
#### *classmethod* parse_iso(s: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Parse from the popular ISO format `YYYY-MM-DDTHH:MM:SS±HH:MM[TZ_ID]`
The inverse of the `format_iso()` method.
```pycon
>>> ZonedDateTime.parse_iso("2020-08-15T23:12:00+01:00[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).
Although it is gaining popularity, it is not yet widely supported.
#### \_\_add_\_(delta: [TimeDelta](reference/time_delta.md#whenever.TimeDelta) | [DateDelta](reference/deprecated.md#whenever.DateDelta) | [DateTimeDelta](reference/deprecated.md#whenever.DateTimeDelta)) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Add an amount of time, accounting for timezone changes (e.g. DST).
See [the docs](https://whenever.rtfd.io/en/latest/guide/arithmetic.html)
for more information.
#### \_\_eq_\_(other: [object](https://docs.python.org/3/library/functions.html#object)) → [bool](https://docs.python.org/3/library/functions.html#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()`](reference/zoned_datetime.md#whenever.ZonedDateTime.exact_eq).
```pycon
>>> 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
```
#### \_\_format_\_(spec: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [str](https://docs.python.org/3/library/stdtypes.html#str)
Default object formatter.
Return str(self) if format_spec is empty. Raise TypeError otherwise.
#### \_\_ge_\_(other: [Instant](reference/instant.md#whenever.Instant) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) | [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare two datetimes by when they occur in time
`a >= b` is equivalent to `a.to_instant() >= b.to_instant()`
```pycon
>>> OffsetDateTime(2020, 8, 15, hour=19, offset=-8) >= (
... ZoneDateTime(2020, 8, 15, hour=20, tz="Europe/Amsterdam")
... )
True
```
#### \_\_gt_\_(other: [Instant](reference/instant.md#whenever.Instant) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) | [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare two datetimes by when they occur in time
`a > b` is equivalent to `a.to_instant() > b.to_instant()`
```pycon
>>> OffsetDateTime(2020, 8, 15, hour=19, offset=-8) > (
... ZoneDateTime(2020, 8, 15, hour=20, tz="Europe/Amsterdam")
... )
True
```
#### \_\_le_\_(other: [Instant](reference/instant.md#whenever.Instant) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) | [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare two datetimes by when they occur in time
`a <= b` is equivalent to `a.to_instant() <= b.to_instant()`
```pycon
>>> OffsetDateTime(2020, 8, 15, hour=23, offset=8) <= (
... ZoneDateTime(2020, 8, 15, hour=20, tz="Europe/Amsterdam")
... )
True
```
#### \_\_lt_\_(other: [Instant](reference/instant.md#whenever.Instant) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) | [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare two datetimes by when they occur in time
`a < b` is equivalent to `a.to_instant() < b.to_instant()`
```pycon
>>> OffsetDateTime(2020, 8, 15, hour=23, offset=8) < (
... ZoneDateTime(2020, 8, 15, hour=20, tz="Europe/Amsterdam")
... )
True
```
#### \_\_str_\_() → [str](https://docs.python.org/3/library/stdtypes.html#str)
Return str(self).
#### \_\_sub_\_(other: [Instant](reference/instant.md#whenever.Instant) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) | [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
#### \_\_sub_\_(other: [TimeDelta](reference/time_delta.md#whenever.TimeDelta)) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Subtract another datetime or duration.
See [the docs](https://whenever.rtfd.io/en/latest/guide/arithmetic.html)
for more information.
#### add(d: [DateTimeDelta](reference/deprecated.md#whenever.DateTimeDelta) | [TimeDelta](reference/time_delta.md#whenever.TimeDelta) | [DateDelta](reference/deprecated.md#whenever.DateDelta) | [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta) | [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta), , , disambiguate: [DisambiguateStr](reference/other-types.md#whenever.DisambiguateStr) = ...) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
#### add(, years: [int](https://docs.python.org/3/library/functions.html#int) = ..., months: [int](https://docs.python.org/3/library/functions.html#int) = ..., weeks: [int](https://docs.python.org/3/library/functions.html#int) = ..., days: [int](https://docs.python.org/3/library/functions.html#int) = ..., hours: [float](https://docs.python.org/3/library/functions.html#float) = ..., minutes: [float](https://docs.python.org/3/library/functions.html#float) = ..., seconds: [float](https://docs.python.org/3/library/functions.html#float) = ..., milliseconds: [float](https://docs.python.org/3/library/functions.html#float) = ..., microseconds: [float](https://docs.python.org/3/library/functions.html#float) = ..., nanoseconds: [int](https://docs.python.org/3/library/functions.html#int) = ..., disambiguate: [DisambiguateStr](reference/other-types.md#whenever.DisambiguateStr) = ...) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Return a new `ZonedDateTime` shifted by the given time amounts
#### IMPORTANT
Shifting by **calendar units** (e.g. months, weeks)
may result in an ambiguous time (e.g. during a DST transition).
Therefore, when adding calendar units, it’s recommended to
specify how to handle such a situation using the `disambiguate` argument.
See [the documentation](https://whenever.rtfd.io/en/latest/guide/arithmetic.html)
for more information.
#### date() → [Date](reference/date.md#whenever.Date)
The date part of the datetime
```pycon
>>> d = PlaineDateTime("2020-01-02 03:04:05")
>>> d.date()
Date("2021-01-02")
```
To perform the inverse, use [`Date.at()`](reference/date.md#whenever.Date.at) and a method
like [`assume_utc()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_utc) or
[`assume_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_tz):
```pycon
>>> date.at(time).assume_tz("Europe/London")
ZonedDateTime("2021-01-02T03:04:05+00:00[Europe/London]")
```
#### day_length() → [TimeDelta](reference/time_delta.md#whenever.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.
```pycon
>>> ZonedDateTime(2020, 8, 15, tz="Europe/London").day_length()
TimeDelta("PT24h")
>>> ZonedDateTime(2023, 10, 29, tz="Europe/Amsterdam").day_length()
TimeDelta("PT25h")
```
#### day_of_year() → [int](https://docs.python.org/3/library/functions.html#int)
Ordinal day in the year (1–366)
```pycon
>>> PlainDateTime(2021, 1, 2).day_of_year()
2
```
#### days_in_month() → [int](https://docs.python.org/3/library/functions.html#int)
Number of days in the current month (28–31)
```pycon
>>> PlainDateTime(2024, 2, 1).days_in_month()
29
```
#### days_in_year() → [int](https://docs.python.org/3/library/functions.html#int)
Number of days in the current year (365 or 366)
```pycon
>>> PlainDateTime(2024, 1, 1).days_in_year()
366
```
#### difference(other: [Instant](reference/instant.md#whenever.Instant) | [OffsetDateTime](reference/offset_datetime.md#whenever.OffsetDateTime) | [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime),) → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
Calculate the exact time difference between two datetimes.
This method returns the exact elapsed [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) between
two instants in time. Equivalent to the subtraction operator (`-`).
Use [`since()`](reference/zoned_datetime.md#whenever.ZonedDateTime.since) or
[`until()`](reference/zoned_datetime.md#whenever.ZonedDateTime.until) for more advanced
options such as calendar units, unit decomposition, and rounding.
#### dst_offset() → [TimeDelta](reference/time_delta.md#whenever.TimeDelta)
The DST offset (adjustment) as a [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta).
```pycon
>>> ZonedDateTime(2020, 8, 15, tz="Europe/London").dst_offset()
TimeDelta("PT1h")
>>> ZonedDateTime(2020, 1, 15, tz="Europe/London").dst_offset()
TimeDelta("PT0s")
```
This value is `TimeDelta.ZERO` when DST is not active:
```pycon
>>> if zoned_dt.dst_offset():
... print("DST is active")
```
#### NOTE
Some timezones have unusual DST rules. For example,
Europe/Dublin defines its standard time as IST (UTC+1) and uses
“negative DST” in winter. In such cases, this method
returns a negative value during winter.
#### end_of(unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['year', 'month', 'week_mon', 'week_sun', 'day', 'hour', 'minute', 'second'],) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
The end of the given unit
```pycon
>>> ZonedDateTime(2024, 8, 15, 14, 30, tz="America/New_York").end_of("day")
ZonedDateTime("2024-08-15 23:59:59.999999999-04:00[America/New_York]")
```
See also [`start_of()`](reference/zoned_datetime.md#whenever.ZonedDateTime.start_of)
#### exact_eq(other: [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime),) → [bool](https://docs.python.org/3/library/functions.html#bool)
Compare objects by their values
(instead of whether they represent the same instant).
Different types are never equal.
```pycon
>>> 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
```
#### NOTE
If `a.exact_eq(b)` is true, then
`a == b` is also true, but the converse is not necessarily true.
#### format(pattern: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [str](https://docs.python.org/3/library/stdtypes.html#str)
Format as a custom pattern string.
See [Pattern format](reference/pattern-format.md#pattern-format) for details.
```pycon
>>> ZonedDateTime(2024, 3, 15, 14, 30, tz="Europe/Paris").format(
... "YYYY-MM-DD hh:mmxxx'['VV']'"
... )
'2024-03-15 14:30+01:00[Europe/Paris]'
```
#### format_iso(, unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond', 'auto'] = 'auto', basic: [bool](https://docs.python.org/3/library/functions.html#bool) = False, sep: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['T', ' '] = 'T', tz: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['always', 'never', 'auto'] = 'always') → [str](https://docs.python.org/3/library/stdtypes.html#str)
Convert to the popular ISO format `YYYY-MM-DDTHH:MM:SS±HH:MM[TZ_ID]`.
The inverse of the `parse_iso()` method.
```pycon
>>> zdt = ZonedDateTime(2020, 8, 15, hour=23, minute=12, tz="Europe/London")
>>> zdt.format_iso(unit="minute", basic=True)
"20200815T2312+0100[Europe/London]"
```
* **Parameters:**
* **unit** – The smallest unit to include in the output.
`"auto"` is the same as `"nanosecond"`,
except that trailing zeroes are omitted from the time part.
* **basic** – Whether to use the basic ISO format (without separators) instead of the extended one.
* **sep** – The separator between the date and time parts.
* **tz** – Whether to include the timezone ID in the output.
`"always"` (default) raises an error if the timezone ID is not available
(in practice, this should only happen for some system timezones without a corresponding IANA timezone ID).
`"auto"` includes the ID if available, and omits it otherwise.
`"never"` always omits the ID.
#### IMPORTANT
The timezone ID is a recent extension to the ISO 8601 format (RFC 9557).
Although it is gaining popularity, it is not yet widely supported
by ISO 8601 parsers.
#### in_leap_year() → [bool](https://docs.python.org/3/library/functions.html#bool)
Whether this date’s year is a leap year
```pycon
>>> PlainDateTime(2024, 1, 1).in_leap_year()
True
```
#### is_ambiguous() → [bool](https://docs.python.org/3/library/functions.html#bool)
Whether the date and time-of-day are ambiguous, e.g. due to a DST transition.
```pycon
>>> ZonedDateTime(2020, 8, 15, 23, tz="Europe/London").is_ambiguous()
False
>>> ZonedDateTime(2023, 10, 29, 2, 15, tz="Europe/Amsterdam").is_ambiguous()
True
```
#### next_transition() → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime) | [None](https://docs.python.org/3/library/constants.html#None)
The next timezone transition after this datetime, if any.
Returns `None` if the timezone has no further transitions
(e.g. for UTC or fixed-offset timezones).
```pycon
>>> d = ZonedDateTime(2024, 1, 1, tz="America/New_York")
>>> d.next_transition()
ZonedDateTime(2024-03-10 03:00:00-04:00[America/New_York])
```
#### prev_transition() → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime) | [None](https://docs.python.org/3/library/constants.html#None)
The previous timezone transition before this datetime, if any.
Returns `None` if the timezone has no earlier transitions
(e.g. for UTC or fixed-offset timezones).
```pycon
>>> d = ZonedDateTime(2024, 1, 1, tz="America/New_York")
>>> d.prev_transition()
ZonedDateTime(2023-11-05 01:00:00-05:00[America/New_York])
```
#### py_datetime() → [datetime](https://docs.python.org/3/library/datetime.html#datetime.datetime)
Convert to a standard library [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime)
#### Deprecated
Deprecated since version 0.10.0: Use [`to_stdlib()`](reference/zoned_datetime.md#whenever.ZonedDateTime.to_stdlib) instead.
#### replace(year: [int](https://docs.python.org/3/library/functions.html#int) = ..., month: [int](https://docs.python.org/3/library/functions.html#int) = ..., day: [int](https://docs.python.org/3/library/functions.html#int) = ..., hour: [int](https://docs.python.org/3/library/functions.html#int) = ..., minute: [int](https://docs.python.org/3/library/functions.html#int) = ..., second: [int](https://docs.python.org/3/library/functions.html#int) = ..., , nanosecond: [int](https://docs.python.org/3/library/functions.html#int) = ..., tz: [str](https://docs.python.org/3/library/stdtypes.html#str) = ..., disambiguate: [DisambiguateStr](reference/other-types.md#whenever.DisambiguateStr) = ...) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Construct a new instance with the given fields replaced.
#### TIP
If you need the start or end of a unit (e.g. the start of the day),
use [`start_of()`](reference/zoned_datetime.md#whenever.ZonedDateTime.start_of) and [`end_of()`](reference/zoned_datetime.md#whenever.ZonedDateTime.end_of) instead.
#### IMPORTANT
Replacing fields of a ZonedDateTime may result in an ambiguous time
(e.g. during a DST transition). Therefore, it’s recommended to
specify how to handle such a situation using the `disambiguate` argument.
By default, if the tz remains the same, the offset is used to disambiguate
if possible, falling back to the “compatible” strategy if needed.
See [the documentation](https://whenever.rtfd.io/en/latest/guide/ambiguity.html)
for more information.
#### replace_date(date: [Date](reference/date.md#whenever.Date), , disambiguate: [DisambiguateStr](reference/other-types.md#whenever.DisambiguateStr) = ...) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Construct a new instance with the date replaced.
See the `replace()` method for more information.
#### replace_time(time: [Time](reference/time.md#whenever.Time), , disambiguate: [DisambiguateStr](reference/other-types.md#whenever.DisambiguateStr) = ...) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Construct a new instance with the time replaced.
See the `replace()` method for more information.
#### round(unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['day', 'hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'] | [TimeDelta](reference/time_delta.md#whenever.TimeDelta) = 'second', , , increment: [int](https://docs.python.org/3/library/functions.html#int) = 1, mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = 'half_even') → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Round the datetime to the specified unit and increment,
or to a multiple of a [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta).
Different rounding modes are available.
```pycon
>>> d = ZonedDateTime("2020-08-15 23:24:18+02:00[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 a repeated time,
the offset is preserved if possible.
Otherwise, ambiguity 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.
#### since(b: [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime), , , total: [DeltaUnitStr](reference/other-types.md#whenever.DeltaUnitStr)) → [float](https://docs.python.org/3/library/functions.html#float)
#### since(b: [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime), , , in_units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DeltaUnitStr')], round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = ..., round_increment: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
Calculate the duration since another ZonedDateTime,
in terms of the specified units.
```pycon
>>> d1 = ZonedDateTime("2020-08-15T23:12:00+01:00[Europe/London]")
>>> d2 = ZonedDateTime("2020-08-14T22:00:00+09:00[Asia/Tokyo]")
>>> d1.since(d2, in_units=["hours", "minutes"],
... round_increment=15,
... round_mode="ceil")
ItemizedDelta("PT33h15m")
```
When calculating calendar units (years, months, weeks, days),
both datetimes must have the same timezone.
#### start_of(unit: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['year', 'month', 'week_mon', 'week_sun', 'day', 'hour', 'minute', 'second'],) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
The start of the given unit
```pycon
>>> ZonedDateTime(2024, 8, 15, 14, 30, tz="America/New_York").start_of("day")
ZonedDateTime("2024-08-15 00:00:00-04:00[America/New_York]")
>>> ZonedDateTime(2024, 8, 15, 14, 30, tz="America/New_York").start_of("hour")
ZonedDateTime("2024-08-15 14:00:00-04:00[America/New_York]")
```
For `"day"`, `"month"`, `"week_mon"`, `"week_sun"`,
and `"year"`, the resulting time
is resolved in the timezone using `"compatible"` disambiguation,
since midnight may not exist due to DST transitions.
For `"hour"`, `"minute"`, and `"second"`, the existing offset
is preserved if valid. A boundary skipped by a transition is moved to
the first valid time after the gap.
#### start_of_day() → [ZonedDateTime](reference/zoned_datetime.md#whenever.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.
#### Deprecated
Deprecated since version 0.10.0: Use `start_of("day")` instead.
#### subtract(d: [DateTimeDelta](reference/deprecated.md#whenever.DateTimeDelta) | [TimeDelta](reference/time_delta.md#whenever.TimeDelta) | [DateDelta](reference/deprecated.md#whenever.DateDelta) | [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta) | [ItemizedDateDelta](reference/itemized_date_delta.md#whenever.ItemizedDateDelta), , , disambiguate: [DisambiguateStr](reference/other-types.md#whenever.DisambiguateStr) = ...) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
#### subtract(, years: [int](https://docs.python.org/3/library/functions.html#int) = ..., months: [int](https://docs.python.org/3/library/functions.html#int) = ..., weeks: [int](https://docs.python.org/3/library/functions.html#int) = ..., days: [int](https://docs.python.org/3/library/functions.html#int) = ..., hours: [float](https://docs.python.org/3/library/functions.html#float) = ..., minutes: [float](https://docs.python.org/3/library/functions.html#float) = ..., seconds: [float](https://docs.python.org/3/library/functions.html#float) = ..., milliseconds: [float](https://docs.python.org/3/library/functions.html#float) = ..., microseconds: [float](https://docs.python.org/3/library/functions.html#float) = ..., nanoseconds: [int](https://docs.python.org/3/library/functions.html#int) = ..., disambiguate: [DisambiguateStr](reference/other-types.md#whenever.DisambiguateStr) = ...) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
The inverse of the `add()` method. See [`add()`](reference/zoned_datetime.md#whenever.ZonedDateTime.add) for more information.
#### time() → [Time](reference/time.md#whenever.Time)
The time-of-day part of the datetime
```pycon
>>> d = ZonedDateTime("2021-01-02T03:04:05+01:00[Europe/Paris])"
>>> d.time()
Time(03:04:05)
```
To perform the inverse, use [`Time.on()`](reference/time.md#whenever.Time.on) and a method
like [`assume_utc()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_utc) or
[`assume_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_tz):
```pycon
>>> time.on(date).assume_tz("Europe/Paris")
ZonedDateTime("2021-01-02T03:04:05+01:00[Europe/Paris]")
```
#### timestamp() → [int](https://docs.python.org/3/library/functions.html#int)
The UNIX timestamp for this datetime. Inverse of [`from_timestamp()`](reference/zoned_datetime.md#whenever.ZonedDateTime.from_timestamp).
```pycon
>>> Instant.from_utc(1970, 1, 1).timestamp()
0
>>> ts = 1_123_000_000
>>> Instant.from_timestamp(ts).timestamp() == ts
True
```
#### 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.
#### timestamp_millis() → [int](https://docs.python.org/3/library/functions.html#int)
Like [`timestamp()`](reference/zoned_datetime.md#whenever.ZonedDateTime.timestamp), but with millisecond precision.
#### timestamp_nanos() → [int](https://docs.python.org/3/library/functions.html#int)
Like [`timestamp()`](reference/zoned_datetime.md#whenever.ZonedDateTime.timestamp), but with nanosecond precision.
#### to_fixed_offset(offset: [int](https://docs.python.org/3/library/functions.html#int) | [TimeDelta](reference/time_delta.md#whenever.TimeDelta) = ...,) → [OffsetDateTime](reference/offset_datetime.md#whenever.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_instant() → [Instant](reference/instant.md#whenever.Instant)
Get the underlying instant in time
```pycon
>>> d = ZonedDateTime(2020, 8, 15, hour=23, tz="Europe/Amsterdam")
>>> d.to_instant()
Instant("2020-08-15 21:00:00Z")
```
#### to_plain() → [PlainDateTime](reference/plain_datetime.md#whenever.PlainDateTime)
Get the underlying date and time without offset or timezone
As an inverse, [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) has methods
[`assume_utc()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_utc), [`assume_fixed_offset()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_fixed_offset)
, [`assume_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_tz), and [`assume_system_tz()`](reference/plain_datetime.md#whenever.PlainDateTime.assume_system_tz).
#### to_stdlib() → [datetime](https://docs.python.org/3/library/datetime.html#datetime.datetime)
Convert to a standard library [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime)
#### NOTE
- Nanoseconds are truncated to microseconds.
If you wish to customize the rounding behavior, use
the `round()` method first.
- For [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) linked to a system timezone without a
IANA timezone ID, the returned Python datetime will have
a fixed offset ([`timezone`](https://docs.python.org/3/library/datetime.html#datetime.timezone) tzinfo)
#### to_system_tz() → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Convert to a ZonedDateTime of the system’s timezone.
#### to_tz(tz: [str](https://docs.python.org/3/library/stdtypes.html#str),) → [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime)
Convert to a ZonedDateTime that represents the same moment in time.
* **Raises:**
[**TimeZoneNotFoundError**](reference/exceptions.md#whenever.TimeZoneNotFoundError) – If the timezone ID is not found in the timezone database.
#### tz_abbrev() → [str](https://docs.python.org/3/library/stdtypes.html#str)
The timezone abbreviation (e.g. `"EST"`, `"CEST"`).
```pycon
>>> ZonedDateTime(2020, 8, 15, tz="Europe/London").tz_abbrev()
'BST'
>>> ZonedDateTime(2020, 1, 15, tz="Europe/London").tz_abbrev()
'GMT'
```
#### WARNING
The abbreviation is often ambiguous and may not be unique,
but it is commonly used in human-readable formats.
Use the timezone ID (e.g. `"Europe/London"`) for unambiguous identification of timezones.
#### until(b: [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime), , , total: [DeltaUnitStr](reference/other-types.md#whenever.DeltaUnitStr)) → [float](https://docs.python.org/3/library/functions.html#float)
#### until(b: [ZonedDateTime](reference/zoned_datetime.md#whenever.ZonedDateTime), , , in_units: [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[TypeAliasForwardRef('DeltaUnitStr')], round_mode: [RoundModeStr](reference/other-types.md#whenever.RoundModeStr) = ..., round_increment: [int](https://docs.python.org/3/library/functions.html#int) = ...) → [ItemizedDelta](reference/itemized_delta.md#whenever.ItemizedDelta)
Inverse of the `since()` method. See [`since()`](reference/zoned_datetime.md#whenever.ZonedDateTime.since) for more information.
#### *property* day *: [int](https://docs.python.org/3/library/functions.html#int)*
The day component of the datetime
#### *property* hour *: [int](https://docs.python.org/3/library/functions.html#int)*
The hour component of the datetime
#### *property* minute *: [int](https://docs.python.org/3/library/functions.html#int)*
The minute component of the datetime
#### *property* month *: [int](https://docs.python.org/3/library/functions.html#int)*
The month component of the datetime
#### *property* nanosecond *: [int](https://docs.python.org/3/library/functions.html#int)*
The nanosecond component of the datetime
#### *property* offset *: [TimeDelta](reference/time_delta.md#whenever.TimeDelta)*
The UTC offset of the datetime
#### *property* second *: [int](https://docs.python.org/3/library/functions.html#int)*
The second component of the datetime
#### *property* tz *: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#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.
#### *property* year *: [int](https://docs.python.org/3/library/functions.html#int)*
The year component of the datetime
# stdlib-pitfalls/broken-equality.md
# `==` ignores `fold`
[PEP 495](https://peps.python.org/pep-0495/) introduced the `fold` attribute to
disambiguate local times during daylight saving time (DST) transitions.
However, to maintain backward compatibility,
the semantics of equality comparisons were not changed to account for this new attribute.
This results in two notable edge cases when comparing aware `datetime` objects:
- **Different time zones, same moment:** Two aware datetimes that represent the same
instant in time but are associated with different time zones may compare as unequal
during DST transitions.
- **Same time zone, different folds:** Two aware datetimes in the same time zone
but with different `fold` values (0 vs 1) may compare as equal, even though they
represent different moments in time during a repeated hour.
The result is equality behavior that is sometimes surprising and occasionally
incorrect from a “moment in time” perspective.
## How `whenever` solves this
`whenever` was designed from the ground up with these considerations in mind.
It defines equality for “aware” objects based on the exact instant in time they represent,
and this holds true consistenly.
```python
>>> dt1 = ZonedDateTime(2024, 10, 27, 2, 30, tz="Europe/Paris", disambiguate="earliest")
>>> dt2 = ZonedDateTime(2024, 10, 27, 2, 30, tz="Europe/Paris", disambiguate="latest")
>>> dt1 == dt2 # different instant, same zone
False
>>> dt1 == dt1.to_tz("Asia/Tokyo") # same instant, different zone
True
```
# stdlib-pitfalls/date-inheritance.md
# `datetime` inherits from `date`—and it breaks
You may be surprised to know that `datetime` is a subclass of `date`.
This doesn’t seem problematic at first, but it leads to odd behavior.
Most notably, the fact that `date` and `datetime` cannot be compared violates
[basic assumptions](https://en.wikipedia.org/wiki/Liskov_substitution_principle) of how subclasses should work.
The `datetime/date` inheritance is now [widely considered](https://discuss.python.org/t/renaming-datetime-datetime-to-datetime-datetime/26279/2)
to be a [design flaw](https://github.com/python/typeshed/issues/4802) in the standard library.
```python
# Breaks on a datetime, even though it's a subclass
def is_future(d: date) -> bool:
return d > date.today()
# Some methods inherited from `date` don't make sense
datetime.today() # fun exercise: what does this return?
```
## How `whenever` solves this
Whenever separates the concepts of date and datetime completely.
There is no inheritance relationship between `Date` and `PlainDateTime`/`ZonedDateTime`.
This means you can catch mistakes at compile time, and all comparisons behave intuitively:
```python
>>> from whenever import Date, PlainDateTime
>>> d = Date(2024, 7, 4)
>>> dt = PlainDateTime(2024, 7, 4, 12, 0, 0)
>>> d > dt # type checker will catch this
```
# stdlib-pitfalls/dst-ignored.md
# `+` ignores DST
Arithmetic with `datetime` usually ignores daylight saving time (DST) transitions,
operating as if the local clock runs uniformly throughout the year.
Here’s an example:
```python
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!
```
You’d expect that going through all the effort of specifying a time zone
would yield correct results around DST transitions.
However, arithmetic is always performed in terms of *local time*,
not *exact (elapsed) time*.
This behavior has surprised many users over the years,
as evidenced by repeated discussions in Python’s issue tracker.
Unfortunately, it cannot be changed without breaking existing code.
What’s more surprising, is that DST *is* considered in some cases.
When subtracting two aware datetimes **with different time zones**:
```python
dt1 - dt2 # DST-aware *only if* time zones differ
```
This means that DST handling depends not just on the operation,
but on whether the time zones involved are the same—an extremely subtle rule.
As a result, a common recommendation is to perform all arithmetic in UTC
and convert to local time only for display.
## How `whenever` solves this
Whenever performs arithmetic in an intuitive, [DST-safe](fundamentals/arithmetic.md#arithmetic2), manner by default:
```python
>>> bedtime = ZonedDateTime(2023, 3, 25, 22, tz="Europe/Paris")
>>> bedtime.add(hours=8)
ZonedDateTime("2023-03-26 07:00:00+02:00[Europe/Paris]") # correct!
```
# stdlib-pitfalls/index.md
# The pitfalls of `datetime`
Python’s `datetime` module first appeared in Python 2.3, back in 2003.
That it has remained usable for over twenty years is remarkable, in a problem
domain this tricky: Java and JavaScript both ended up replacing their
date-time APIs wholesale, while Python never needed a wholesale replacement.
It does, however, have sharp edges which regularly trip up even experienced developers.
Below are the most impactful ones, and what `whenever` does instead.
#### NOTE
None of this is a condemnation of `datetime`. It has been carefully maintained
and adapted over the years—all while preserving backwards compatibility.
“Pitfall” is a subjective term: what follows is simply a catalog
of the places where the design makes certain mistakes easy to make.
One class for two incompatible concepts, so annotations can’t tell
them apart
Eight hours after 10pm isn’t always 6am, but `+` thinks it is
Sometimes the system timezone, sometimes UTC, sometimes neither
Times that happen twice—or never—are resolved without a word
Identical moments can compare unequal, and distinct ones equal
Several timezone classes to choose from; the obvious one is wrong
The result depends on your machine’s configuration
A subclass that can’t be compared with its own base class
A remainder that looks like a total—right up until it doesn’t
# stdlib-pitfalls/naive-aware.md
# One type for two incompatible concepts
Python uses a single `datetime` type to represent two fundamentally different concepts:
* **Naive datetimes**, which have no time zone information
* **Aware datetimes**, which are associated with a time zone
These two behave differently, are interpreted differently,
and should never be mixed.
Yet the type system makes no distinction between them.
This becomes especially frustrating in typed code.
There is no way to express, using type annotations,
whether a function expects a naive or an aware `datetime`.
```python
def schedule_at(dt: datetime) -> None:
...
```
Does `dt` represent a local wall-clock time? A UTC timestamp? A zoned time?
The type gives you no way to say.
This makes it impossible to statically enforce one of the most important
invariants in date-time code.
As a result, mistakes that should be caught early often surface only
at runtime—or worse, much later.
## How `whenever` solves this
`whenever` strictly separates datetimes with and without time zone information:
* [`PlainDateTime`](reference/plain_datetime.md#whenever.PlainDateTime) is the equivalent of a “naive” time
* [`ZonedDateTime`](reference/zoned_datetime.md#whenever.ZonedDateTime) is the equivalent of an “aware” time with
`ZoneInfo` attached
* [`Instant`](reference/instant.md#whenever.Instant) is the equivalent of an “aware” time with `UTC` attached
This makes type annotations precise and self-documenting:
```python
def schedule_at(dt: ZonedDateTime) -> None:
...
```
# stdlib-pitfalls/naive-meaning.md
# “Naive” is interpreted differently in different places
In various parts of the standard library, “naive” datetimes are interpreted differently.
Ostensibly, “naive” means “detached from the real world”,
but in the datetime library it is often implicitly treated as the system timezone.
Confusingly, it is sometimes treated as UTC, while in other places it is treated as neither!
```python
# a naive datetime
d = datetime(2024, 1, 1)
# here: treated as in the system timezone
d.timestamp()
d.astimezone(UTC)
# here: assumed to be UTC
d.utctimetuple()
email.utils.format_datetime(d)
datetime.utcnow()
# here: neither! (error)
d >= datetime.now(UTC)
```
This inconsistency leads to subtle bugs when naive datetimes are used in different contexts.
Since neither the type system nor runtime checks can know the intended meaning of a naive datetime,
it’s easy to accidentally mix interpretations.
Thankfully, methods like [`utcnow()`](https://docs.python.org/3/library/datetime.html#datetime.datetime.utcnow) are being deprecated, slowly making “system timezone”
the only implicit meaning of naive datetimes in the standard library.
But this behavior [also has drawbacks](stdlib-pitfalls/system-timezone.md#stdlib-system-tz).
## How `whenever` solves this
Whenever’s `PlainDateTime` type is always explicitly detached from any timezone.
It never assumes any implicit meaning, and cannot be mixed with timezone-aware types
without explicit conversion:
```python
>>> d = PlainDateTime("2024-07-04 12:36:56")
>>> d.assume_utc()
Instant("2024-07-04 12:36:56Z")
>>> d.assume_system_tz()
ZonedDateTime("2024-07-04 12:36:56+02:00[Europe/Berlin]")
```
# stdlib-pitfalls/silent-ambiguity.md
# Ambiguity is resolved without a word
When time zone offsets change—most commonly due to daylight saving time—a local
clock time may occur **twice** or **not at all**.
Python resolves these ambiguities with the `fold` parameter, which defaults to `0`.
That’s not inherently wrong: having a deterministic default [is often useful](fundamentals/ambiguity.md#ambiguity-default).
The problem is that it’s difficult to handle ambiguity *explicitly*:
* There is no option to raise an error (instead of picking a default)
* The `fold` parameter is subtle and poorly discoverable
* Many users are unaware ambiguity exists at all
```python
# does `fold` matter here? Hard to tell!
datetime(2024, 10, 27, 2, 30, tzinfo=ZoneInfo("Europe/Amsterdam"), fold=1)
```
Without careful handling, code may interpret an ambiguous local time
differently than intended.
## How `whenever` solves this
While `whenever` also defaults to the same convention as Python (as do most libraries),
it provides explicit tools to handle ambiguity:
```python
>>> dt = ZonedDateTime(2024, 10, 27, 2, 30, tz="Europe/Amsterdam", disambiguate="raise")
Traceback (most recent call last):
...
RepeatedTime: 2024-10-27 02:30:00 is repeated in timezone 'Europe/Amsterdam'
```
# stdlib-pitfalls/system-timezone.md
# The system time zone is used implicitly
In the standard library,
converting to the system time zone is implicit–and often the default.
While this may be convenient in some cases,
it’s hardly something you want to depend on in most applications.
In most cases, it’s a surprise to developers when their code
suddenly depends on the system configuration.
For example, you may be surprised to learn that the output of these lines
depend on the system time zone:
```python
>>> datetime.fromtimestamp(t) # returns a naive datetime in system tz
>>> my_datetime.astimezone(None) # converts to system tz if no tz is given
>>> date.today() # returns a date in the system tz
```
This implicit behavior makes it hard to see when code is depending on the system configuration.
Many applications do not need the system time zone at all—but can stumble into it accidentally.
Worse, the system time zone obtained this way is represented as a **fixed offset** or **naive**,
not a full set of rules.
That means the resulting datetime is not safe for arithmetic across DST transitions.
## How `whenever` solves this
Whenever makes converting to the system time zone an explicit operation,
and never assumes this intention implicitly.
This is the case when converting from a naive datetime:
```python
>>> from whenever import PlainDateTime
>>> dt = PlainDateTime(2024, 3, 10, 15, 0, 0)
>>> dt.assume_system_tz()
ZonedDateTime("2024-03-10 15:00:00-05:00[America/New_York]")
```
or when converting from a moment in time:
```python
>>> now = Instant.now()
>>> now.to_system_tz()
ZonedDateTime("2024-03-10 10:30:00-05:00[America/New_York]")
```
The resulting time zone always has the full knowledge of DST rules and historical changes,
making it safe for further operations.
# stdlib-pitfalls/timedelta-seconds.md
# `timedelta.seconds` footgun
After subtracting two datetimes, you get a [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta).
You may want to know how many seconds it represents.
```python
delta = end - start
delta.seconds
```
This looks reasonable at first—but it’s almost never what you want.
This is because the `.seconds` attribute is a **remainder, not a total**.
While the remainder looks like the total number of seconds in many cases…
```python
>>> d = timedelta(seconds=123)
>>> d.seconds
123 # looks good, right?
```
…it breaks down for negative or durations longer than one day:
```python
>>> d = timedelta(seconds=-1)
>>> d.seconds
86399 # huh?
>>> d = timedelta(hours=25)
>>> d.seconds
3600 # also huh? (25 hours is 90000 seconds)
```
This is due to the internal representation of [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta),
which stores values in [`days`](https://docs.python.org/3/library/datetime.html#datetime.timedelta.days), [`seconds`](https://docs.python.org/3/library/datetime.html#datetime.timedelta.seconds),
and [`microseconds`](https://docs.python.org/3/library/datetime.html#datetime.timedelta.microseconds) fields.
Only the `days` field can be negative.
If you need the duration in seconds, the correct method is
[`total_seconds()`](https://docs.python.org/3/library/datetime.html#datetime.timedelta.total_seconds).
Unfortunately, the attribute name `seconds` makes the wrong choice too tempting,
and the problem often only shows up with negative or large durations.
## How `whenever` solves this
Whenever’s [`TimeDelta`](reference/time_delta.md#whenever.TimeDelta) hides its internal representation
and provides a single way to get the duration in various units:
```python
>>> from whenever import TimeDelta
>>> delta = TimeDelta(hours=2)
>>> delta.total("seconds")
7200.0
>>> delta.total("minutes")
120.0
```
# stdlib-pitfalls/timezone-classes.md
# `timezone` isn’t a time zone
Python offers multiple time zone-related classes, and choosing the right one is not obvious.
Your first instinct might be **[`datetime.timezone`](https://docs.python.org/3/library/datetime.html#datetime.timezone)**. After all, its name suggests it represents time zones.
However, this class only supports fixed offsets from UTC. This is not nearly enough to
represent real-world time zones, which have complex rules for daylight saving time and historical changes.
Perhaps you should use the **`pytz.timezone`** class from the popular third-party library `pytz`?
You could, but it has a [notoriously tricky API](https://blog.ganssle.io/articles/2018/03/pytz-fastest-footgun.html)
that can lead to mistakes if not used carefully.
In the end, what you probably want is the slightly jargon-y **[`zoneinfo.ZoneInfo`](https://docs.python.org/3/library/zoneinfo.html#zoneinfo.ZoneInfo)** class from the standard library,
introduced in Python 3.9. This class provides access to the IANA time zone database,
allowing you to work with real-world time zones accurately.
The reason for this confusion is historical:
when `datetime` was designed, the IANA time zone database was not as widely adopted as it is today.
In the meantime, third-party libraries like `pytz` filled the gap.
Today, `zoneinfo` is the correct choice for most applications—but the older
names remain, and `pytz` is still widely used, adding to the confusion.
## How `whenever` solves this
Whenever uses the IANA time zone database by default:
```python
>>> from whenever import ZonedDateTime
>>> zdt = ZonedDateTime(2024, 3, 10, 15, tz="America/New_York")
ZonedDateTime("2024-03-10 15:00:00-04:00[America/New_York]")
```
Additionally, fixed-offset datetimes are explicitly represented with a separate class,
so there’s no confusion which class to use for full-featured time zones versus fixed offsets:
```python
>>> from whenever import OffsetDateTime
>>> odt = OffsetDateTime(2024, 3, 10, 15, offset=-4)
OffsetDateTime("2024-03-10 15:00:00-04:00")
```
# why-not-pendulum.md
# Why not Pendulum?
[**Pendulum**](https://pypi.org/project/pendulum/)
is a popular third-party datetime library that
arrived on the scene in 2016, promising better DST handling.
It offers a convenient API, useful formatting and localization features,
and more intuitive arithmetic than the standard library in several common cases.
However, Pendulum is best understood as an extension of `datetime`,
not a redesign of it. Because it subclasses `datetime` and `timedelta`,
it inherits their shortcomings along with their compatibility.
It then adds calendar units and implicit policies that these base classes
cannot always preserve consistently.
As a result, equality, hashing, arithmetic, parsing, and serialization can
disagree about what a value means.
#### NOTE
This section is up to date as of Pendulum version 3.2.0.
## It improves ergonomics, but retains `datetime`’s model
Pendulum does address some of the standard library’s
[pitfalls](stdlib-pitfalls/index.md#datetime-pitfalls).
In particular, its named arithmetic methods distinguish calendar units from
elapsed units across DST transitions, and `Duration.seconds` avoids the
[timedelta.seconds footgun](stdlib-pitfalls/timedelta-seconds.md#timedelta-seconds).
Most of the underlying model remains unchanged, though:
- naive and aware datetimes use the same class;
- `DateTime` inherits from `date`;
- equality and ordering retain `datetime`’s DST edge cases.
Pendulum’s description of its classes as
[“drop-in replacements”](https://pendulum.eustace.io/docs/#introduction)
therefore needs qualification. The same documentation acknowledges a
[non-exhaustive list of incompatibilities](https://pendulum.eustace.io/docs/#limitations)
with libraries that distinguish types exactly, including database drivers.
Pendulum’s policy of preferring aware datetimes is not consistently enforced
throughout the API:
```python
>>> import pendulum
>>> pendulum.datetime(2020, 1, 1)
DateTime(2020, 1, 1, 0, 0, 0, tzinfo=Timezone('UTC'))
>>> pendulum.DateTime(2020, 1, 1)
DateTime(2020, 1, 1, 0, 0, 0)
```
Some inherited states are not handled consistently either.
For example, a naive `DateTime` reports that it is in daylight saving time,
even though `dst()` correctly reports that DST is undefined:
```python
>>> naive = pendulum.DateTime(2024, 1, 1)
>>> naive.dst() is None
True
>>> naive.is_dst()
True
```
## Missing timezone information becomes UTC
When parsing a datetime without an offset, Pendulum assumes UTC:
```python
>>> import pendulum
>>> pendulum.parse("2024-03-10T15:00")
DateTime(2024, 3, 10, 15, 0, 0, tzinfo=Timezone('UTC'))
```
An ISO 8601 datetime without an offset does not identify an instant.
It only supplies local date and time fields.
Treating it as UTC invents information that was absent from the input.
If the source intended another time zone, this can silently shift the
resulting instant by several hours.
The same policy is used when converting a naive standard-library datetime:
```python
>>> from datetime import datetime
>>> pendulum.instance(datetime(2024, 3, 10, 15))
DateTime(2024, 3, 10, 15, 0, 0, tzinfo=Timezone('UTC'))
```
## Calendar arithmetic violates basic invariants
Pendulum’s `Duration` adds years and months to `timedelta`.
That is a difficult fit: `timedelta` represents an exact elapsed duration,
while a calendar month has no fixed length.
Pendulum approximates a month as 30 days for compatibility with `timedelta`.
At the same time, applying a month to a datetime uses calendar arithmetic.
Consequently, values that compare equal are not interchangeable:
```python
>>> import pendulum
>>> month = pendulum.duration(months=1)
>>> thirty_days = pendulum.duration(days=30)
>>> month == thirty_days
True
>>> hash(month) == hash(thirty_days)
True
>>> jan_31 = pendulum.datetime(2024, 1, 31)
>>> jan_31 + month
DateTime(2024, 2, 29, 0, 0, 0, tzinfo=Timezone('UTC'))
>>> jan_31 + thirty_days
DateTime(2024, 3, 1, 0, 0, 0, tzinfo=Timezone('UTC'))
```
Even identity operations can erase calendar information:
```python
>>> month + pendulum.duration()
Duration(weeks=4, days=2)
>>> month * 1
Duration(months=1)
>>> month * 1.0
Duration()
```
So can ordinary serialization and shallow copying:
```python
>>> import copy
>>> import pickle
>>> copy.copy(month)
Duration(weeks=4, days=2)
>>> pickle.loads(pickle.dumps(month))
Duration(weeks=4, days=2)
```
Compatibility with the `timedelta` base class is also asymmetric:
```python
>>> from datetime import timedelta
>>> day = pendulum.duration(days=1)
>>> timedelta(hours=1) / day
0.041666666666666664
>>> day / timedelta(hours=1)
Traceback (most recent call last):
...
AttributeError: 'datetime.timedelta' object has no attribute '_to_microseconds'
```
The same private-method assumption affects floor division, modulo, and
`divmod()`. This has been reported for `Interval` since 2019
([#382](https://github.com/python-pendulum/pendulum/issues/382)).
### `Interval` violates equality and hashing contracts
`Interval`, the result of subtracting two Pendulum datetimes,
is itself a subclass of `Duration`.
Its equality semantics do not form a valid equivalence relation:
```python
>>> jan = pendulum.interval(
... pendulum.datetime(2024, 1, 1),
... pendulum.datetime(2024, 1, 2),
... )
>>> one_day = pendulum.duration(days=1)
>>> feb = pendulum.interval(
... pendulum.datetime(2024, 2, 1),
... pendulum.datetime(2024, 2, 2),
... )
>>> jan == one_day
True
>>> one_day == feb
True
>>> jan == feb
False
```
Equality is therefore not transitive.
Equal objects can also have different hashes:
```python
>>> jan == one_day
True
>>> hash(jan) == hash(one_day)
False
>>> len({jan, one_day})
2
```
This violates Python’s contract for hashable objects and can produce incorrect
behavior in dictionaries, sets, caches, and deduplication code.
## `Time` arithmetic can discard information
Pendulum exposes aware `Time` values, but its arithmetic converts through an
epoch datetime and returns only the clock fields. The timezone is silently
lost:
```python
>>> paris = pendulum.timezone("Europe/Paris")
>>> noon = pendulum.Time(12, tzinfo=paris)
>>> noon.add(hours=1)
Time(13, 0, 0)
>>> noon.add(hours=1).tzinfo is None
True
```
Subtracting two times has a separate precision problem:
```python
>>> late = pendulum.Time(12, 0, 0, 900_000)
>>> early = pendulum.Time(12, 0, 0, 100_000)
>>> late - early
Duration()
```
The expected difference is 800,000 microseconds. The implementation calculates
whole seconds and omits both operands’ microseconds, an issue reported in
[#362](https://github.com/python-pendulum/pendulum/issues/362) and
[#584](https://github.com/python-pendulum/pendulum/issues/584).
## DST handling remains inconsistent
Pendulum’s named `add()` and `subtract()` methods are a useful
improvement over `datetime`.
They distinguish calendar days from elapsed hours and handle many DST
transitions conveniently.
Equivalent-looking operations do not always take the same path:
```python
>>> import pendulum
>>> dt = pendulum.datetime(2013, 4, 2, tz="Europe/Paris")
>>> three_days = pendulum.duration(days=3)
>>> dt.subtract(days=3)
DateTime(2013, 3, 30, 0, 0, 0, tzinfo=Timezone('Europe/Paris'))
>>> dt - three_days
DateTime(2013, 3, 29, 23, 0, 0, tzinfo=Timezone('Europe/Paris'))
>>> dt + -three_days
DateTime(2013, 3, 30, 0, 0, 0, tzinfo=Timezone('Europe/Paris'))
```
Here, `dt - delta` differs from both `dt.subtract(...)` and `dt + -delta`.
A fix is proposed in
[pull request #987](https://github.com/python-pendulum/pendulum/pull/987).
### Comparisons can contradict chronological order
During a repeated hour, ordering compares local fields instead of instants:
```python
>>> later = pendulum.datetime(
... 2023, 11, 5, 1, 15,
... tz="America/Los_Angeles",
... fold=1,
... )
>>> earlier = pendulum.datetime(
... 2023, 11, 5, 1, 25,
... tz="America/Los_Angeles",
... fold=0,
... )
>>> later.timestamp() > earlier.timestamp()
True
>>> later < earlier
True
```
This contradicts Pendulum’s documentation, which says comparisons account for
time zones.
The problem was first reported in
[#351](https://github.com/python-pendulum/pendulum/issues/351);
another fix is currently proposed in
[#985](https://github.com/python-pendulum/pendulum/pull/985).
Intervals spanning the repeated hour are internally inconsistent as well.
For an interval of exactly one elapsed hour, Pendulum 3.2.0 can report:
```python
>>> first = pendulum.datetime(
... 2023, 11, 5, 1, 25,
... tz="America/Los_Angeles",
... fold=0,
... )
>>> second = pendulum.datetime(
... 2023, 11, 5, 1, 25,
... tz="America/Los_Angeles",
... fold=1,
... )
>>> interval = second - first
>>> interval.total_seconds()
3600.0
>>> interval.in_seconds()
3600
>>> interval.hours
0
>>> interval.in_words()
'0 microseconds'
```
### Serialization can change the instant
Pickling does not preserve `fold`.
Serializing a datetime in the second occurrence of a repeated hour
and reading it back can therefore change its timestamp:
```python
>>> import pickle
>>> original = pendulum.datetime(
... 2024, 11, 3, 1,
... tz="America/Chicago",
... fold=1,
... )
>>> restored = pickle.loads(pickle.dumps(original))
>>> original.fold, restored.fold
(1, 0)
>>> original.timestamp() == restored.timestamp()
False
```
Shallow copying follows the same reconstruction path and has the same effect;
`deepcopy()` does preserve the fold.
This is tracked in [#908](https://github.com/python-pendulum/pendulum/issues/908).
An open fix is available in
[pull request #909](https://github.com/python-pendulum/pendulum/pull/909).
### Its default disambiguation differs from the common convention
Pendulum defaults to `fold=1`, selecting the offset *after* a backwards
transition.
Python’s standard library and the convention used by most datetime libraries
default to the offset before the transition; see
[the discussion of ambiguity defaults](fundamentals/ambiguity.md#ambiguity-default).
Pendulum allows callers to choose a `fold`, and
`raise_on_unknown_times=True` can reject ambiguous or nonexistent local times.
The default is significant: moving code from `datetime` to Pendulum can change
which instant an ambiguous local time represents unless the fold is chosen
explicitly.
## Parsing is permissive and implementation-dependent
`parse()` handles several unrelated kinds of input.
Depending on the string, it may return a `DateTime`, `Duration`, or `Interval`.
Time-only input is particularly surprising:
```python
>>> type(pendulum.parse("12:34:56")).__name__
'DateTime'
>>> type(pendulum.parse("12:34:56", exact=True)).__name__
'Time'
```
Without `exact=True`, Pendulum supplies the current date.
The same parser also gives the string `"now"` special, undocumented,
time-dependent behavior:
```python
>>> pendulum.parse("now").timezone_name
'UTC'
```
This makes the result depend on the wall clock rather than only on the input.
It can therefore be risky when parsing user-controlled or externally supplied
strings.
Parsing behavior also depends on whether Pendulum’s Rust extension is loaded.
On the normal compiled build:
```python
>>> pendulum.parse("PT4294967297M")
Duration(minutes=1)
>>> pendulum.parse("P12M4M")
Duration(months=4)
```
The first value has overflowed, while the second is not a valid ISO 8601
duration.
With `PENDULUM_EXTENSIONS=0`, the pure-Python parser preserves all
4,294,967,297 minutes and rejects the duplicated month component.
The implementations can also assign different values to valid input:
```python
# Compiled parser
>>> pendulum.parse("P1.25D")
Duration(days=1, hours=6)
# Pure-Python parser
>>> pendulum.parse("P1.25D")
Duration(days=3, hours=12)
```
The pure parser divides the fractional digits by ten regardless of their
length, so `1.25` days is interpreted as `3.5` days. This parser bug has
remained open since 2021
([#534](https://github.com/python-pendulum/pendulum/issues/534)).
The project currently lacks systematic parity tests between the two
implementations, as acknowledged in
[#907](https://github.com/python-pendulum/pendulum/issues/907).
## The documentation is outdated and incomplete
Pendulum’s documentation is primarily a guide, rather than a complete API
reference. A request for a usable reference has remained open since 2018
([#199](https://github.com/python-pendulum/pendulum/issues/199)).
Several published examples no longer match version 3.2.0:
- the timezone guide still recommends `dst_rule`, `PRE_TRANSITION`,
`POST_TRANSITION`, and `TRANSITION_ERROR`, all removed in 3.0
([#789](https://github.com/python-pendulum/pendulum/issues/789));
- parts of the documentation still call the result of `diff()` a `Period`,
although the public class is now `Interval`;
- examples for `Duration.total_days()` disagree with the actual result;
- the introduction says that comparisons account for time zones, which the
repeated-hour example above contradicts.
## Maintenance remains a concern
Pendulum is active again after a long period of limited maintenance, and recent
releases and contributions are welcome signs.
However, reproducible correctness issues remain unresolved across releases,
including cases where a tested fix is already available.
Examples include the pending fixes for pickling, naive datetime differences,
floating-point `Duration` multiplication, fold ordering, and DST subtraction
([#909](https://github.com/python-pendulum/pendulum/pull/909),
[#968](https://github.com/python-pendulum/pendulum/pull/968),
[#975](https://github.com/python-pendulum/pendulum/pull/975),
[#985](https://github.com/python-pendulum/pendulum/pull/985),
and [#987](https://github.com/python-pendulum/pendulum/pull/987)).
## Performance has regressed
While Pendulum initially promised
[improved performance](https://pendulum.eustace.io/faq/),
users have reported a version 3
[`in_tz()` performance regression](https://github.com/python-pendulum/pendulum/issues/818).
In [benchmarks](performance.md#benchmarks), Pendulum is often an order of magnitude
slower than both the standard library and `whenever`.
Correctness and predictable semantics matter more than speed for datetime
code. The additional overhead may nevertheless matter to applications
performing large volumes of datetime operations.