NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
PyPI · #1611 most downloaded on PyPI
Type-safe datetimes for Python that get DST right, in Rust or pure Python
Last release 2 days ago
02 Oct 2026
Release timing varies
gaps range from 8 days to 2 months
Nearly every release is documented
notes for 59 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
3 years old
89 releases · first in 2023
…while retaining compatibility shims for newly deprecated interfaces. Unless significant issues arise, this API will become 1.0 after those deprecated…
This release is intended as a soft 1.0 release: it establishes the planned 1.0 API while retaining compatibility shims for newly deprecated interfaces. Unless significant issues arise, this API will become 1.0 after those deprecated interfaces are removed.
Getting the API right for 1.0 means breaking a few things first. Two changes you'll notice: the system time zone API, which now uses a PEP 661 sentinel, and a round of renames that gives the library one consistent vocabulary.
Thanks for bearing with the churn. This is the last of it: once 1.0 locks in, the API stays put, and releases bring only bug fixes and new features.
This release already brings plenty of the former. It irons out a long list of bugs, most of them in corners: daylight saving time transitions, unusual time zone files, the ends of the supported range, and inputs on which the two backends disagreed. The documentation got a thorough polish too, with a new glossary and a comparison with Arrow.
Breaking changes
The system time zone is now accepted everywhere a named time zone (i.e. tz=) is accepted, using the new SYSTEM_TZ sentinel (PEP 661). The methods specific to the system time zone are deprecated: to_system_tz(), assume_system_tz(), Date.today_in_system_tz(), ZonedDateTime.now_in_system_tz(), and ZonedDateTime.from_system_tz().
ty, pyrefly and mypy 2.4 and later type-check SYSTEM_TZ.
Rationale: one sentinel lets the regular time zone APIs cover the system time zone without duplicating every operation. It also makes call-time system time zone resolution explicit and takes advantage of the sentinel pattern recently standardized by PEP 661.
Several public names are clarified, and the old spellings are deprecated. See the migration table at the end of this entry.
Rationale: the new names describe their concepts more precisely and use one vocabulary across the API. TZPATH was a module attribute that was loaded on access; its replacement, get_tzpath(), is explicit about this.
Removed APIs deprecated before 0.11: DateDelta, DateTimeDelta, the years(), months(), weeks(), and days() helpers, legacy standard library conversion methods, TimeDelta.in_*() convenience methods, Date.days_since() and Date.days_until(), deprecated Date operators, parse_strptime(), ZonedDateTime.start_of_day(), ignore_dst, and ImplicitlyIgnoringDST.
See the 0.10.0 entry below for migration instructions.
Timestamp APIs are consolidated around a unit= argument. Instant.from_timestamp(..., unit=) and exact-time .timestamp(unit=) support seconds, milliseconds, microseconds, and nanoseconds. timestamp_millis(), timestamp_nanos(), and their matching Instant factories are deprecated, as are the timestamp factories on OffsetDateTime and ZonedDateTime: construct an Instant first, then call to_fixed_offset() or to_tz().
Rationale: one unit-selectable API is easier to discover and extend, while Instant is the natural type for constructing an exact time from a timestamp.
Resolving a repeated or skipped local time without an explicit disambiguation= now emits ImplicitDisambiguationWarning. The "compatible" default still applies, so the result is unchanged, but a warning filter set to error now raises. Pass disambiguation="compatible" to silence the warning.
Rationale: an unstated disambiguation silently picks one of two instants. A sensible default is fine, but the choice should be explicit.
Fixed-offset arguments passed as bare integers signifying hours are deprecated. Use TimeDelta or a factory like hours() instead.
Rationale: the unit of a bare integer is implicit, which makes offset values easy to misread or misuse.
Patterns use H/HH for the 24-hour clock; h/hh are deprecated. Optional seconds go in brackets after mm: [:ss], [:ss.fff], [:ss.FFF], or [ss] without a separator. The SS spellings remain with deprecation warnings through 0.11. Patterns and parsed input strings are ASCII-only.
Rationale: H/HH is the near-universal spelling for a 24-hour specifier, and brackets make optional seconds and their separator explicit. Where brackets may appear is set out in the pattern reference.
Unpickling a ZonedDateTime preserves its instant under the time zone rules of the loading environment. If those rules give a different offset, the local time follows and a PickleOffsetMismatchWarning is emitted.
Rationale: time zone databases change, and unpickling can't take parameters, so preserving the instant is the sensible default. The warning ensures it doesn't pass silently.
Constructing a ZonedDateTime from a standard library datetime resolves its offset the way ISO parsing does: an offset that disagrees with the time zone rules raises InvalidOffsetError by default, and offset_mismatch= and disambiguation= resolve it.
Pattern offsets without seconds round an offset with seconds to the nearest minute, as Temporal does. Hour-only x/X patterns reject offsets that still have minutes after rounding.
An ItemizedDelta and an ItemizedDateDelta with the same components are equal: ItemizedDelta(days=3) == ItemizedDateDelta(days=3). strict_eq() takes only a delta of its own type, and raises TypeError for the other.
Itemized delta add() and subtract() are simplified: they compose, and take no reference. relative_to= is deprecated, with in_units=, round_mode=, round_increment=, naive_arithmetic_ok=, and stale_offset_ok=; call in_units() on the result instead. The warning is now more precise: CalendarUnitCompositionWarning is now MonthCompositionWarning, escaped with month_composition_ok=, and only years and months trigger it.
Rationale: relative_to= was in_units() combined with add(), and it silenced the warning without avoiding the actual pitfall: it summed the deltas before applying them, as composition always does.
ItemizedDateDelta.in_units() returns an ItemizedDateDelta whatever the reference, where a datetime reference gave an ItemizedDelta.
Two lossy calls now warn: Date() given a datetime, whose time it drops (call .date() first), and OffsetDateTime.since()/until() where a calendar-unit result depends on the offset held fixed (StaleOffsetWarning).
A few meaningless arguments are rejected: a time with a tzinfo in Time(), a "week" rounding unit anywhere but TimeDelta.round(), an increment= alongside a TimeDelta unit in round(), and pattern fractions followed by a digit field. A non-integer increment= raises TypeError, as do bytes given to YearMonth.parse_iso() and IsoWeekDate.parse_iso(), and a .FFF pattern no longer parses a bare trailing dot. reset_tzpath() takes only a list or tuple: the search path is ordered, and a set gave a different one in each process.
Added and improved
US/Eastern are preserved.patch_current_time() and its TimePatch handle, with shift() and move_to(), are now part of the stable API.offset_mismatch= to ZonedDateTime parsing. A numeric offset is matched at its written precision, while Z always identifies an exact UTC instant. OffsetDateTime.assume_tz() supports the same policy and uses disambiguation= when retaining local time.Date.today(tz), YearMonth.add() and subtract(), and day_of_week() on PlainDateTime, OffsetDateTime, and ZonedDateTime.ItemizedDelta.total(). ItemizedDelta and ItemizedDateDelta are hashable, so they can be set members and dict keys.in_units() and total() on ItemizedDelta and TimeDelta with a PlainDateTime or OffsetDateTime as relative_to= accept the escape their warning names: naive_arithmetic_ok= or stale_offset_ok=.llms.txt.rust-extension=skip, which configuration files can set: config-settings-package in uv's pyproject.toml, --config-settings in requirements.txt. WHENEVER_NO_BUILD_RUST_EXT still works.TimeDelta() accepts timedelta subclasses and ZonedDateTime() a datetime with a ZoneInfo subclass, like the other standard library overloads.DeltaTotalUnitStr and TimestampUnitStr. from whenever import * now includes all the *Str aliases.Fixed
start_of(), end_of(), round(), day_length(), and since()/until() agree on where a day starts when a gap or a repeated midnight gets in the way, and units no longer overlap in a fold.ZonedDateTime.replace() and calendar add()/subtract() could jump to the other occurrence of a repeated time, even when it wasn't necessary. They now keep the current offset where it's still valid, as parsing does.round_increment= now rounds the smallest unit, as it does with calendar units.TimeZoneNotFoundError, where it could crash, fall back to another zone's rules, or load and give wrong results.TZ is a path into a zoneinfo directory, or when /etc/localtime is a copy of a database file named by /etc/timezone.TZ variable, or no TZ and no /etc/localtime, means UTC, as it does for the C library, instead of raising.tzlocal, reset_system_tz() actually determines the system time zone again.round() rounded to the wrong side in a few rare cases.TimeDelta by a float rounds to the nearest nanosecond, where it truncated.dst_offset() was zero in some historical periods, and next_transition()/prev_transition() could stop where nothing changed or skip a transition.pandas Timestamp, is read through its standard library fields, with a WheneverWarning about the data this loses.OffsetDateTime.replace(offset=...) no longer emits StaleOffsetWarning.help() on Rust extension methods that take keyword arguments showed no documentation.Instant pickled by the pure-Python backend of 0.8.0 to 0.10.0 loads correctly in the Rust extension.nanoseconds reached a whole second.Date + ItemizedDateDelta and sorted() on mixed exact-time types, and reject disambiguation= where it can't apply.TimeZoneNotFoundError wherever it's given, including inside an ISO string.Instant.parse() accepted a weekday that contradicts the date.-0000 (unknown offset) for a negative offset under a minute.time-machine installed, the Rust extension's now() was off before 1970 and raised past 2262.patch_current_time before the core types no longer raises a circular-import ImportError.TimePatch handle could still move the clock after its patch had ended.available_timezones() no longer follows symlinks to directories, as zoneinfo doesn't.'' inside a quoted run is a literal quote, and X parses a lowercase z, as ISO 8601 parsing does.repr(Weekday.MONDAY) is Weekday.MONDAY.VV without a time zone ID consistently raises, and IsoWeekDate.parse_iso() accepts a lowercase w.format_iso(basic=...) reads its flag by truthiness, like every other flag.Migration summary. Each spelling in this table warns at runtime. A type checker that implements PEP 702 (@deprecated) also flags each deprecated method, keyword, and value at the call site; TZPATH, DisambiguateStr, CalendarUnitCompositionWarning, and the pattern specifiers warn at runtime only:
| Deprecated spelling | Preferred spelling |
|---|---|
disambiguate= |
disambiguation= |
DisambiguateStr |
DisambiguationStr |
from_timestamp_millis(v) |
from_timestamp(v, unit="millisecond") |
from_timestamp_nanos(v) |
from_timestamp(v, unit="nanosecond") |
timestamp_millis() |
timestamp(unit="millisecond") |
timestamp_nanos() |
timestamp(unit="nanosecond") |
ZonedDateTime.from_timestamp(v, tz=tz) |
Instant.from_timestamp(v).to_tz(tz) |
ZonedDateTime.from_timestamp_millis(v, tz=tz) |
Instant.from_timestamp(v, unit="millisecond").to_tz(tz) |
ZonedDateTime.from_timestamp_nanos(v, tz=tz) |
Instant.from_timestamp(v, unit="nanosecond").to_tz(tz) |
OffsetDateTime.from_timestamp(v, offset=o) |
Instant.from_timestamp(v).to_fixed_offset(o) |
OffsetDateTime.from_timestamp_millis(v, offset=o) |
Instant.from_timestamp(v, unit="millisecond").to_fixed_offset(o) |
OffsetDateTime.from_timestamp_nanos(v, offset=o) |
Instant.from_timestamp(v, unit="nanosecond").to_fixed_offset(o) |
format_iso(tz="always") |
format_iso(tz_id_display="required") |
format_iso(tz="auto") |
format_iso(tz_id_display="if_available") |
format_iso(tz="never") |
format_iso(tz_id_display="omit") |
to_system_tz() |
to_tz(SYSTEM_TZ) |
assume_system_tz() |
assume_tz(SYSTEM_TZ) |
Date.today_in_system_tz() |
Date.today(SYSTEM_TZ) |
ZonedDateTime.now_in_system_tz() |
ZonedDateTime.now(SYSTEM_TZ) |
ZonedDateTime.from_system_tz(...) |
ZonedDateTime(..., tz=SYSTEM_TZ) |
ZonedDateTime.tz |
ZonedDateTime.tz_id |
delta.add(other, relative_to=r, in_units=u) |
delta.add(other, month_composition_ok=True).in_units(u, relative_to=r) |
cal_unit_composition_ok= |
month_composition_ok= |
CalendarUnitCompositionWarning |
MonthCompositionWarning |
exact_eq() |
strict_eq() |
parse(..., format=p) |
parse(..., pattern=p) |
pattern h / hh |
H / HH |
pattern :SS |
[:ss] |
pattern :SS.fff |
[:ss.fff] |
pattern :SS.FFF |
[:ss.FFF] |
separator-free pattern SS |
[ss] |
offset=2, replace(offset=2), to_fixed_offset(2), assume_fixed_offset(2), OffsetDateTime.now(2) |
hours(2) in place of 2 |
MonthDay.is_leap() |
MonthDay.is_leap_day() |
ZonedDateTime.is_ambiguous() |
ZonedDateTime.is_repeated() |
TZPATH |
get_tzpath() |
[:ss.fff] isn't a pure rename of :SS.fff: with zero seconds and fraction, the old spelling emitted a dangling 12:00.000 instead of 12:00.
tz_id is typed str | None, where tz was typed str. Code that passed .tz where a str was required now needs a check or an assertion.
One column per month.
update mypy so sentinel is supported
update mypy so sentinel is supported
add() and subtract() on ItemizedDelta and ItemizedDateDelta only compose now. relative_to= is deprecated, along with in_units= , round_mode= , round_i…
Changes since 0.11.0b1.
Itemized deltas
add() and subtract() on ItemizedDelta and ItemizedDateDelta only compose now. relative_to= is deprecated, along with in_units=, round_mode=, round_increment=, naive_arithmetic_ok=, and stale_offset_ok=: call in_units() on the result instead.CalendarUnitCompositionWarning is renamed to MonthCompositionWarning, and its escape cal_unit_composition_ok= to month_composition_ok=. Only years and months trigger it now. The old names still work, with a deprecation warning.Other changes
reset_tzpath() takes only a list or tuple. The search path is ordered, and a set gave a different order in each process.format_rfc2822() rounds offset seconds to the nearest minute, as the xx pattern does, instead of truncating them.X pattern specifier parses a lowercase z, as ISO 8601 parsing does.parse_iso() on YearMonth, MonthDay, and IsoWeekDate raises TypeError for a non-string argument, where bytes gave a misleading ValueError.from whenever import * includes all the *Str type aliases.Fixed
patch_current_time before the core types raised a circular-import ImportError.TimePatch handle could still move the clock after its patch had ended.available_timezones() followed symlinks to directories, unlike zoneinfo.since() and until() on ZonedDateTime could round a tie the wrong way next to a skipped day.…while retaining compatibility shims for newly deprecated interfaces. Unless significant issues arise, this API will become 1.0 after those deprecated…
The upcoming 0.11 release is intended as a soft 1.0: it establishes the planned 1.0 API while retaining compatibility shims for newly deprecated interfaces. Unless significant issues arise, this API will become 1.0 after those deprecated interfaces are removed.
Getting the API right for 1.0 means breaking a few things first. Two changes you'll notice: the system time zone API, which now uses a PEP 661 sentinel, and a round of renames that gives the library one consistent vocabulary.
Thanks for bearing with the churn. This is the last of it: once 1.0 locks in, the API stays put, and releases bring only bug fixes and new features.
This release already brings plenty of the former. It irons out a long list of bugs, most of them in corners: daylight saving time transitions, unusual time zone files, the ends of the supported range, and inputs on which the two backends disagreed. The documentation got a thorough polish too, with a new glossary and a comparison with Arrow.
Breaking changes
The system time zone is now accepted everywhere a named time zone (i.e. tz=) is accepted, using the new SYSTEM_TZ sentinel (PEP 661). The methods specific to the system time zone are deprecated: to_system_tz(), assume_system_tz(), Date.today_in_system_tz(), ZonedDateTime.now_in_system_tz(), and ZonedDateTime.from_system_tz().
ty, pyrefly and mypy 2.4 and later type-check SYSTEM_TZ.
Rationale: one sentinel lets the regular time zone APIs cover the system time zone without duplicating every operation. It also makes call-time system time zone resolution explicit and takes advantage of the sentinel pattern recently standardized by PEP 661.
Several public names are clarified, and the old spellings are deprecated. See the migration table at the end of this entry.
Rationale: the new names describe their concepts more precisely and use one vocabulary across the API. TZPATH was a module attribute that was loaded on access; its replacement, get_tzpath(), is explicit about this.
Removed APIs deprecated before 0.11: DateDelta, DateTimeDelta, the years(), months(), weeks(), and days() helpers, legacy standard library conversion methods, TimeDelta.in_*() convenience methods, Date.days_since() and Date.days_until(), deprecated Date operators, parse_strptime(), ZonedDateTime.start_of_day(), ignore_dst, and ImplicitlyIgnoringDST.
See the 0.10.0 entry below for migration instructions.
Timestamp APIs are consolidated around a unit= argument. Instant.from_timestamp(..., unit=) and exact-time .timestamp(unit=) support seconds, milliseconds, microseconds, and nanoseconds. timestamp_millis(), timestamp_nanos(), and their matching Instant factories are deprecated, as are the timestamp factories on OffsetDateTime and ZonedDateTime: construct an Instant first, then call to_fixed_offset() or to_tz().
Rationale: one unit-selectable API is easier to discover and extend, while Instant is the natural type for constructing an exact time from a timestamp.
Resolving a repeated or skipped local time without an explicit disambiguation= now emits ImplicitDisambiguationWarning. The "compatible" default still applies, so the result is unchanged, but a warning filter set to error now raises. Pass disambiguation="compatible" to silence the warning.
Rationale: an unstated disambiguation silently picks one of two instants. A sensible default is fine, but the choice should be explicit.
Fixed-offset arguments passed as bare integers signifying hours are deprecated. Use TimeDelta or a factory like hours() instead.
Rationale: the unit of a bare integer is implicit, which makes offset values easy to misread or misuse.
Patterns use H/HH for the 24-hour clock; h/hh are deprecated. Optional seconds go in brackets after mm: [:ss], [:ss.fff], [:ss.FFF], or [ss] without a separator. The SS spellings remain with deprecation warnings through 0.11. Patterns and parsed input strings are ASCII-only.
Rationale: H/HH is the near-universal spelling for a 24-hour specifier, and brackets make optional seconds and their separator explicit. Where brackets may appear is set out in the pattern reference.
Unpickling a ZonedDateTime preserves its instant under the time zone rules of the loading environment. If those rules give a different offset, the local time follows and a PickleOffsetMismatchWarning is emitted.
Rationale: time zone databases change, and unpickling can't take parameters, so preserving the instant is the sensible default. The warning ensures it doesn't pass silently.
Constructing a ZonedDateTime from a standard library datetime resolves its offset the way ISO parsing does: an offset that disagrees with the time zone rules raises InvalidOffsetError by default, and offset_mismatch= and disambiguation= resolve it.
Pattern offsets without seconds round an offset with seconds to the nearest minute, as Temporal does. Hour-only x/X patterns reject offsets that still have minutes after rounding.
An ItemizedDelta and an ItemizedDateDelta with the same components are equal: ItemizedDelta(days=3) == ItemizedDateDelta(days=3). strict_eq() still tells them apart.
Arithmetic on date-only deltas stays date-only: composing two of them, or ItemizedDateDelta.in_units(), returns an ItemizedDateDelta whatever the reference, where a datetime reference gave an ItemizedDelta.
Two lossy calls now warn: Date() given a datetime, whose time it drops (call .date() first), and OffsetDateTime.since()/until() where a calendar-unit result depends on the offset held fixed (StaleOffsetWarning).
A few meaningless arguments are rejected: a time with a tzinfo in Time(), a "week" rounding unit anywhere but TimeDelta.round(), an increment= alongside a TimeDelta unit in round(), and pattern fractions followed by a digit field. A non-integer increment= raises TypeError, and a .FFF pattern no longer parses a bare trailing dot.
Added and improved
US/Eastern are preserved.patch_current_time() and its TimePatch handle, with shift() and move_to(), are now part of the stable API.offset_mismatch= to ZonedDateTime parsing. A numeric offset is matched at its written precision, while Z always identifies an exact UTC instant. OffsetDateTime.assume_tz() supports the same policy and uses disambiguation= when retaining local time.Date.today(tz), YearMonth.add() and subtract(), and day_of_week() on PlainDateTime, OffsetDateTime, and ZonedDateTime.ItemizedDelta.total(). ItemizedDelta and ItemizedDateDelta are hashable, so they can be set members and dict keys.ItemizedDelta.add() and subtract() accept a PlainDateTime or OffsetDateTime as relative_to=, as in_units() does. Calls with such a reference accept the escape their warning names: naive_arithmetic_ok= or stale_offset_ok=.llms.txt and llms-full.txt.rust-extension=skip, which configuration files can set: config-settings-package in uv's pyproject.toml, --config-settings in requirements.txt. WHENEVER_NO_BUILD_RUST_EXT still works.TimeDelta() accepts timedelta subclasses and ZonedDateTime() a datetime with a ZoneInfo subclass, like the other standard library overloads.DeltaTotalUnitStr and TimestampUnitStr.Fixed
start_of(), end_of(), round(), day_length(), and since()/until() agree on where a day starts when a gap or a repeated midnight gets in the way, and units no longer overlap in a fold.ZonedDateTime.replace() and calendar add()/subtract() could jump to the other occurrence of a repeated time, even when it wasn't necessary. They now keep the current offset where it's still valid, as parsing does.round_increment= now rounds the smallest unit, as it does with calendar units.TimeZoneNotFoundError, where it could crash, fall back to another zone's rules, or load and give wrong results.TZ is a path into a zoneinfo directory, or when /etc/localtime is a copy of a database file named by /etc/timezone.TZ variable, or no TZ and no /etc/localtime, means UTC, as it does for the C library, instead of raising.tzlocal, reset_system_tz() actually determines the system time zone again.round() rounded to the wrong side in a few rare cases.TimeDelta by a float rounds to the nearest nanosecond, where it truncated.dst_offset() was zero in some historical periods, and next_transition()/prev_transition() could stop where nothing changed or skip a transition.pandas Timestamp, is read through its standard library fields, with a WheneverWarning about the data this loses.OffsetDateTime.replace(offset=...) no longer emits StaleOffsetWarning.help() on Rust extension methods that take keyword arguments showed no documentation.Instant pickled by the pure-Python backend of 0.8.0 to 0.10.0 loads correctly in the Rust extension.nanoseconds reached a whole second, and an ItemizedDateDelta with a datetime reference failed with an AssertionError.Date + ItemizedDateDelta and sorted() on mixed exact-time types, and reject disambiguation= where it can't apply.TimeZoneNotFoundError wherever it's given, including inside an ISO string.Instant.parse() accepted a weekday that contradicts the date.-0000 (unknown offset) for a negative offset under a minute.time-machine installed, the Rust extension's now() was off before 1970 and raised past 2262.reset_tzpath() given an iterator set an empty search path.'' inside a quoted run is a literal quote.repr(Weekday.MONDAY) is Weekday.MONDAY.VV without a time zone ID consistently raises, and IsoWeekDate.parse_iso() accepts a lowercase w.format_iso(basic=...) reads its flag by truthiness, like every other flag.Migration summary. Each spelling in this table warns at runtime. A type checker that implements PEP 702 (@deprecated) also flags each deprecated method, keyword, and value at the call site; TZPATH, DisambiguateStr, and the pattern specifiers warn at runtime only:
| Deprecated spelling | Preferred spelling |
|---|---|
disambiguate= |
disambiguation= |
DisambiguateStr |
DisambiguationStr |
from_timestamp_millis(v) |
from_timestamp(v, unit="millisecond") |
from_timestamp_nanos(v) |
from_timestamp(v, unit="nanosecond") |
timestamp_millis() |
timestamp(unit="millisecond") |
timestamp_nanos() |
timestamp(unit="nanosecond") |
ZonedDateTime.from_timestamp(v, tz=tz) |
Instant.from_timestamp(v).to_tz(tz) |
ZonedDateTime.from_timestamp_millis(v, tz=tz) |
Instant.from_timestamp(v, unit="millisecond").to_tz(tz) |
ZonedDateTime.from_timestamp_nanos(v, tz=tz) |
Instant.from_timestamp(v, unit="nanosecond").to_tz(tz) |
OffsetDateTime.from_timestamp(v, offset=o) |
Instant.from_timestamp(v).to_fixed_offset(o) |
OffsetDateTime.from_timestamp_millis(v, offset=o) |
Instant.from_timestamp(v, unit="millisecond").to_fixed_offset(o) |
OffsetDateTime.from_timestamp_nanos(v, offset=o) |
Instant.from_timestamp(v, unit="nanosecond").to_fixed_offset(o) |
format_iso(tz="always") |
format_iso(tz_id_display="required") |
format_iso(tz="auto") |
format_iso(tz_id_display="if_available") |
format_iso(tz="never") |
format_iso(tz_id_display="omit") |
to_system_tz() |
to_tz(SYSTEM_TZ) |
assume_system_tz() |
assume_tz(SYSTEM_TZ) |
Date.today_in_system_tz() |
Date.today(SYSTEM_TZ) |
ZonedDateTime.now_in_system_tz() |
ZonedDateTime.now(SYSTEM_TZ) |
ZonedDateTime.from_system_tz(...) |
ZonedDateTime(..., tz=SYSTEM_TZ) |
ZonedDateTime.tz |
ZonedDateTime.tz_id |
exact_eq() |
strict_eq() |
parse(..., format=p) |
parse(..., pattern=p) |
pattern h / hh |
H / HH |
pattern :SS |
[:ss] |
pattern :SS.fff |
[:ss.fff] |
pattern :SS.FFF |
[:ss.FFF] |
separator-free pattern SS |
[ss] |
offset=2, replace(offset=2), to_fixed_offset(2), assume_fixed_offset(2), OffsetDateTime.now(2) |
hours(2) in place of 2 |
MonthDay.is_leap() |
MonthDay.is_leap_day() |
ZonedDateTime.is_ambiguous() |
ZonedDateTime.is_repeated() |
TZPATH |
get_tzpath() |
[:ss.fff] isn't a pure rename of :SS.fff: with zero seconds and fraction, the old spelling emitted a dangling 12:00.000 instead of 12:00.
tz_id is typed str | None, where tz was typed str. Code that passed .tz where a str was required now needs a check or an assertion.
Improve arrow/pendulum docs
Improve arrow/pendulum docs
Add binary wheels for Python 3.15.
Support addition and subtraction on itemized deltas without a reference date(time). This operation is performed itemwise and emits a CalendarUnitCompo
Added
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.+ and - operators between date(times) and itemized deltas.TimeDelta + datetime) wherever the corresponding datetime + TimeDelta operation is supported.WheneverWarning as the base class for all warnings emitted by whenever, allowing package-wide suppression or escalation.Fixed
ValueError.Nothing published for this version
Fixed the pure Python implementation accepting invalid basic-format times with a separatorless fraction (e.g. 20200101 or 20103000 ). These now raise
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).24:00, trailing duration separators, and overflow in the Rust duration parsers.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)Fixed an issue in the pure Python implementation where invalid format patterns containing a trimmed-fraction ( F ) field would raise an AttributeError
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).
Reduced import time by ~60% when the Rust extension is active. This was achieved by deferring the import of several internal submodules and timezone d
Improved
Added
"week_mon" and "week_sun" as valid units for start_of() and end_of() on Date, PlainDateTime, ZonedDateTime, and OffsetDateTime.Fixed
whenever.__all__ and ensured dir(whenever) includes lazily loaded public attributes without importing them.Date.today_in_system_tz() in the Rust extension so it uses whenever's cached system timezone instead of from datetime.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.Nothing published for this version
Nothing published for this version
migrate black/isort/flake8 to ruff
migrate black/isort/flake8 to ruff
A big release with several breaking changes and improvements. Highlights are the new delta API, customizable string formatting and parsing, and since(…
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:
DateDelta(...) with ItemizedDateDelta(...).DateTimeDelta(...) with ItemizedDelta(...).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))..in_months_days() and .in_months_days_secs_nanos() with .in_units(['months', 'days']) and .in_units(['months', 'days', 'seconds', 'nanoseconds']), respectively.Date +/- operators with DateDelta are deprecated; use add()/subtract() instead.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:
ignore_dst=True with the appropriate keyword argument:
OffsetDateTime methods: stale_offset_ok=TruePlainDateTime methods: naive_arithmetic_ok=TrueTimeDelta methods: days_assumed_24h_ok=Truewarnings.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
60), normalizing them to 59. This applies to ISO 8601, RFC 2822, and custom format strings.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).since() and until() methods on Date, ZonedDateTime, OffsetDateTime, and PlainDateTime for calculating the difference between two values in terms of specific calendar/time units.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.TimeDelta.total() and TimeDelta.in_units() methods for converting a time delta into specific units.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.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).Date(datetime.date(2024, 1, 1)).ZonedDateTime.dst_offset() and ZonedDateTime.tz_abbrev() methods for querying timezone metadata (DST offset adjustment and timezone abbreviation).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.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().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().PlainDateTime, ZonedDateTime, OffsetDateTime: day_of_year(), days_in_month(), days_in_year(), in_leap_year(), start_of(), end_of().YearMonth: days_in_month(), days_in_year(), in_leap_year().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
< operator between Time instances if nanoseconds are involved.Nothing published for this version
A big release with several breaking changes and improvements. Highlights are the new delta API, customizable string formatting and parsing, and since(…
View the docs of this pre-release version of whenever here. Do you have feedback on the changes? Post in discussions
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:
DateDelta(...) with ItemizedDateDelta(...).DateTimeDelta(...) with ItemizedDelta(...).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))..in_months_days() and .in_months_days_secs_nanos()
with .in_units(['months', 'days']) and .in_units(['months', 'days', 'seconds', 'nanoseconds']), respectively.Date +/- operators with DateDelta are deprecated;
use add()/subtract() instead.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:
ignore_dst=True with the appropriate keyword argument:
OffsetDateTime methods: stale_offset_ok=TruePlainDateTime methods: naive_arithmetic_ok=TrueTimeDelta methods: days_assumed_24h_ok=Truewarnings.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
60),
normalizing them to 59. This applies to ISO 8601, RFC 2822,
and custom format strings.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).since() and until() methods on Date, ZonedDateTime,
OffsetDateTime, and PlainDateTime for calculating the difference
between two values in terms of specific calendar/time units.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.TimeDelta.total() and TimeDelta.in_units() methods for
converting a time delta into specific units.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.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).Date(datetime.date(2024, 1, 1)).ZonedDateTime.dst_offset()
and ZonedDateTime.tz_abbrev() methods for querying timezone metadata
(DST offset adjustment and timezone abbreviation).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.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().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().PlainDateTime, ZonedDateTime,
OffsetDateTime: day_of_year(), days_in_month(),
days_in_year(), in_leap_year(), start_of(), end_of().YearMonth: days_in_month(),
days_in_year(), in_leap_year().YearMonth, MonthDay, Weekday, and IsoWeekDate are now
implemented in pure Python always, reducing the compiled extension size.Fixed
< operator between Time
instances if nanoseconds are involved.Changed during beta
d to E to avoid confusion with D.difference() method is no longer deprecated.Nothing published for this version
Beta of the next big release (0.10) with several breaking changes and improvements. Highlights are the new delta API, customizable string formatting a…
View the docs of this pre-release version of whenever here. Do you have feedback on the changes? Post in discussions
Beta of the next big release (0.10) 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:
DateDelta(...) with ItemizedDateDelta(...).DateTimeDelta(...) with ItemizedDelta(...).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))..in_months_days() and .in_months_days_secs_nanos()
with the Mapping interface (e.g. delta['months'])
or .in_units().Date +/- operators with DateDelta are deprecated;
use add()/subtract() instead.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 context managers or Python's standard warning filters.
Rationale: The ignore_dst parameter was a source of confusion, and made the OffsetDateTime APIs less compatible.
Migration:
ignore_dst=True from all calls.OffsetDateTime operations: use with ignore_potentially_stale_offset_warning(): ... to suppress warnings.PlainDateTime operations: use with ignore_timezone_unaware_arithmetic_warning(): ... to suppress warnings.warnings.filterwarnings() to suppress PotentiallyStaleOffsetWarning or TimeZoneUnawareArithmeticWarning.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 whenever 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.difference() method on datetimes is deprecated. Instead, use the - subtraction operator, or the new since() method.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.Improved/added
since() and until() methods on Date, ZonedDateTime, OffsetDateTime, and PlainDateTime for calculating the difference between two values in terms of specific calendar/time units.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.TimeDelta.total() and TimeDelta.in_units() methods for converting a time delta into specific units.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.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).Date(datetime.date(2024, 1, 1)).ZonedDateTime.dst_offset() and ZonedDateTime.tz_abbrev() methods for querying timezone metadata (DST offset adjustment and timezone abbreviation).PotentiallyStaleOffsetWarning, TimeZoneUnawareArithmeticWarning, DaysNotAlways24HoursWarning) and corresponding context managers (ignore_potentially_stale_offset_warning(), ignore_timezone_unaware_arithmetic_warning(), ignore_days_not_always_24h_warning()) for fine-grained control over DST-related warnings.Fixed
< operator between Time instances if nanoseconds are involved.Nothing published for this version
Fix issue where not all windows wheels were built and uploaded
Fix issue where not all windows wheels were built and uploaded (#317)
Nothing published for this version
Added support for free-threaded Python
Added support for free-threaded Python (#166)
Nothing published for this version
Nothing published for this version
Nothing published for this version
Fixed incorrect offsets for some timezones before the start of their first recorded transition (typically pre-1950)
Fixed incorrect offsets for some timezones before the start of their first recorded transition (typically pre-1950) (#296)
Fixed incorrect offsets for some time zones before the start of their first recorded transition (typically pre-1950) (#296)
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 im
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)
Added ZonedDateTime.now_in_system_tz() and ZonedDateTime.from_system_tz() as convenience methods to ease migration away from SystemDateTime.
Added ZonedDateTime.now_in_system_tz() and ZonedDateTime.from_system_tz() as convenience methods to ease migration away from SystemDateTime.
Renamed [format|parse]_common_iso methods to [format|parse]_iso. The old methods are still available (but deprecated) to ease the transition.
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:
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.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.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:
ZonedDateTime representing a system time are
now orders of magnitude faster than they were on the old SystemDateTime.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:
SystemDateTime with ZonedDateTime in all type hints.SystemDateTime.now() with ZonedDateTime.now_in_system_tz() (whenever >=0.9.1)
or Instant.now().to_system_tz() (whenever <0.9.1)SystemDateTime(...) constructor calls with ZonedDateTime.from_system_tz(...)
(whenever >=0.9.1) or PlainDateTime(...).assume_system_tz() (whenever <0.9.1).to_system_tz() and .assume_system_tz(): these
methods now return a ZonedDateTime instance. In most cases,
no code change is needed.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
PlainDateTime constructor raising TypeError instead of
ValueError when passed invalid parameters../ are now properly rejected. Other path traversal
attempts were already handled correctly.RuntimeError insteadRemove the deprecated local() methods (use to_plain() instead).
First release candidate for the next big release: 0.9.0.
Breaking Changes
SystemDateTime has been removed and its functionality is now integrated into ZonedDateTime.
Migration:
SystemDateTime.now() can be replaced with Instant.now().to_system_tz().to_system_tz() and assume_system_tz() now return a ZonedDateTime instead of a SystemDateTime.Rationale: The SystemDateTime class was an awkward corner of the API,
creating inconsistencies and overlapping with ZonedDateTime.
This change unifies the API, providing a single, consistent way to handle
all timezone-aware datetimes. The original use cases are fully supported
by the improved ZonedDateTime.
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).
The repr() of all classes now includes quotes, so that the output
can be directly used as input and thus eval(repr(obj)) == obj.
Rationale: This makes the types a lot easier to use in interactive sessions
and tests. It also makes repr() round-trippable, which is a common
expectation for primitive types.
Renamed [format|parse]_common_iso methods to [format|parse]_iso.
Rationale: The "common" qualifier is no longer necessary because these methods have been expanded to handle a wider range of ISO 8601 formats.
Remove the deprecated local() methods (use to_plain() instead).
Remove the deprecated instant() method (use to_instant() instead).
Improved
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
PlainDateTime constructor raising TypeError instead of
ValueError when passed invalid parameters../ are now properly rejected. Other path traversal
attempts were already handled correctly.Backport of timezone fixes from 0.9.x to 0.8.x that affected some timezones far into the future or past
Backport of timezone fixes from 0.9.x to 0.8.x that affected some timezones far into the future or past
Fixed not all test files included in source distribution
0.8.9 is a small release while I work on 0.9.0. See here for the changes expected in the next major release.
Nothing published for this version
Add wheels for Python 3.14 now that its ABI is stable.
whenever's pure Python version without having to go through the source build process (#256)Fix some MIN and MAX constants not documented in the API reference.
MIN and MAX constants not documented in the API reference.Time.MIN alias for Time.MIDNIGHT for consistency (#245)ZonedDateTime values in "ceil"/day mode (#249)Improve error message of ZonedDateTime.from_py_datetime() in case the datetime's ZoneInfo.key is None.
ZonedDateTime.from_py_datetime() in case the datetime's ZoneInfo.key is None.Date.day_of_week() (#244)Relax build requirements. It now only depends on setuptools_rust if opting to build the Rust extension
setuptools_rust if opting to build the Rust extension (#240)Fix Pydantic JSON schema generation in certain contexts which affected FastAPI doc generation.
Fix Pydantic JSON schema generation in certain contexts which affected FastAPI doc generation.
Nothing published for this version
Nothing published for this version
Ensure Pydantic parsing failures of whenever types always result in a proper ValidationError, not a TypeError.
Ensure Pydantic parsing failures of whenever types always result in a proper ValidationError, not a TypeError.
Allow Pydantic to generate JSON schema for whenever types. This is particularly useful for generating OpenAPI schemas for FastAPI.
Allow Pydantic to generate JSON schema for whenever types. This is particularly useful for generating OpenAPI schemas for FastAPI.
Added support for Pydantic serialization/deserialization of whenever types in the ISO 8601 format. This functionality is in preview, and may be subjec
New
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.TimeDelta seconds (#234)Improved
Time.from_py() now ignores any tzinfo, instead of raising an error.unsafe code, making it safer and more idiomatic.Nothing published for this version
A big release with several improvements and breaking changes that lay the groundwork for the eventual 1.0 release.
A big release with several improvements and breaking changes that lay the groundwork for the eventual 1.0 release.
Improved
zoneinfo module. (#202)parse_common_iso() methods support a wider range of ISO 8601
formats. See the updated documentation for details.
(#204)Breaking changes
There are several breaking changes. For the full rationale behind them, see the changelog
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.
Rename instant() method to to_instant()
Removed the [format|parse]_rfc3339 method.
Passing invalid timezone names now raise a
whenever.TimeZoneNotFoundError (subclass of ValueError) instead of
zoneinfo.ZoneInfoNotFoundError (subclass of KeyError).
TimeDelta.from_py_timedelta no longer accepts timedelta
subclasses.
The strptime methods have been renamed parse_strptime,
and its format argument is now a keyword-only argument.
The InvalidOffset exception has been renamed InvalidOffsetError
SkippedTime and RepeatedTime are now subclasses of ValueError.
Whenever is no longer affected by ZoneInfo.clear_cache() or
zoneinfo.reset_tzpath(), since it now uses its own cache with
corresponding methods.
Fixed
ZonedDateTime.exact_eq()
that could cause false positives in some cases.day_length() and start_of_day()
methods.now(). (#213)A big release with several improvements and breaking changes that lay the groundwork for the eventual 1.0 release.
Improved
zoneinfo module. (#202)parse_common_iso() methods support a wider range of ISO 8601
formats. See the updated documentation for details.
(#204)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
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 time zone 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
ZonedDateTime.exact_eq()
that could cause false positives in some cases.day_length() and start_of_day()
methods.now(). (#213)Fixed type annotations of Weekday enum values, so they are properly marked as int. (thanks @pmdevita)
Fixed type annotations of Weekday enum values, so they are properly marked as int. (thanks @pmdevita)
Fixed round() method behaving incorrectly when increment argument is not passed explicitly
Fixed round() method behaving incorrectly when increment argument is not passed explicitly (#209)
Date.add and Date.subtract now support DateDelta to be passed as sole positional argument.
Date.add and Date.subtract now support DateDelta to be passed as sole positional argument.Date.add and Date.subtract now support DateDelta to be passed as
sole positional argument. This is consistent with the behavior of
datetime classes.This release adds rounding functionality, along with a small breaking change (see below).
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
round() to all datetime, Instant, and TimeDelta classesTimeDeltais_ambiguous(), day_length() and start_of_day() to SystemDateTime, for consistency with ZonedDateTime.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 imple
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.Make disambiguate argument optional in all methods, defaulting to "compatible".
disambiguate argument optional in all methods, defaulting to "compatible".ZonedDateTime repr() that would mangle some timezone namesFix bug in ZonedDateTime repr() that would mangle some time zone
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
Add Date.days_[since|until] methods for calculating the difference between two dates in days only (no months or years)
Date.days_[since|until] methods for calculating the difference between two dates in days only (no months or years)Ensure docstrings and error messages are consistent in Rust extension as well as the pure-Python version
Fixed
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.Make from_py_datetime() methods of Instant/OffsetDateTime less pedantic. They now accept any aware datetime
Added
from_py_datetime() methods of Instant/OffsetDateTime less pedantic. They now accept any aware datetimeDate.today_in_system_tz() convenience methodFixed
06:79) now raises the expected parsing failure.parse_rfc2822() docstring that it doesn't (yet) validate the input, due to limitations in the underlying parser.Fixed format_rfc3339() docs that incorrectly displayed a T separator. Clarified that T can be added by using the format_common_iso() method instead.
Fixed format_rfc3339() docs that incorrectly displayed a T separator. Clarified that T can be added by using the format_common_iso() method instead. (#185)
format_rfc3339() docstrings that incorrectly included a T
separator. Clarified that T can be added by using the
format_common_iso() method instead. (#185)(new) Added YearMonth and MonthDay classes for working with year-month and month-day pairs
YearMonth and MonthDay classes for working with year-month and month-day pairswhenever.__version__ is now also accessible when Rust extension is usedImprove method documentation and autocomplete support (#172, #173, #176)
Improved
Fixed
offset on InstantLocalDateTime.difference return type annotationClarify DST-related error messages
Clarify DST-related error messages (#169)
Fix object deallocation bug that caused a crash in rare cases
Fix object deallocation bug that caused a crash in rare cases (#167)
Your coding agent can read these notes before it upgrades. Set up the MCP server →