Delta types¶
Tip
For the motivation behind the three types and help choosing one, start with Working with deltas. This page documents their complete behavior.
Overview¶
Feature |
|||
|---|---|---|---|
exact units |
calendar units |
exact and calendar units |
|
yes |
no |
no |
|
n/a |
n/a |
||
with |
with |
||
n/a |
|||
Applies to… |
|||
Similar to… |
Exact and calendar units¶
The delta types accept these unit categories:
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).
See the fundamentals for exact versus calendar semantics and Working with deltas for choosing a delta type.
Normalized or “itemized”¶
ItemizedDateDelta and ItemizedDelta store their individual
components
(years, months, weeks, days, hours, minutes, seconds, nanoseconds) separately,
without normalizing them into each other.
You can imagine this working like a dict or Counter of components,
where each unit is a key and its value is the corresponding amount:
>>> dict(d)
{'hours': 1, 'minutes': 90}
Unlike a Counter, an absent component raises KeyError rather than
counting as zero, and + keeps zero components instead of dropping them.
Iteration always runs from the largest unit to the smallest and includes only the components you gave. Explicit zeroes remain present:
>>> list(ItemizedDelta(seconds=0, hours=2))
['hours', 'seconds']
The one exception is seconds, which nanoseconds brings with it; see
Seconds and nanoseconds.
TimeDelta instead normalizes all its components into one exact
duration. See Working with deltas for a side-by-side example.
For the same reason, TimeDelta has no replace(): a normalized
delta is one quantity, so build a new one or add to it. The itemized deltas
replace and remove components with replace():
>>> ItemizedDelta(hours=1, minutes=90).replace(minutes=None, seconds=30)
ItemizedDelta("PT1h30s")
Seconds and nanoseconds¶
Seconds and nanoseconds are one quantity written as two components, the way
ISO 8601 writes PT1.5S as one number. Keeping 90 minutes unbalanced
against hours preserves something you asked for; keeping nanoseconds
unbalanced against seconds would preserve nothing. Five rules follow:
There are no
millisecondsormicrosecondscomponents. Usetotal()for a scalar in those units. It returns afloat, except for"nanoseconds", which returns anint:>>> d = ItemizedDelta(seconds=1, nanoseconds=234_567_890) >>> reference = PlainDateTime(2024, 1, 1) >>> d.total("milliseconds", relative_to=reference) 1234.56789 >>> d.total("microseconds", relative_to=reference) 1234567.89 >>> d.total("nanoseconds", relative_to=reference) 1234567890
nanosecondsis bounded to 999,999,999, under the delta’s single sign. Whole seconds go inseconds:>>> ItemizedDelta(nanoseconds=1_500_000_000) ValueError: nanoseconds must be within ±999,999,999; put whole seconds in seconds=
A present
nanosecondsbrings a presentseconds, because the ISO 8601 fraction needs a seconds value to attach to:>>> dict(ItemizedDelta(nanoseconds=5)) {'seconds': 0, 'nanoseconds': 5}
Presence survives the ISO 8601 round trip.
seconds=0formats asPT0S, whilenanoseconds=0formats asPT0.0S, and parsing reads the fraction back as a presentnanoseconds:>>> ItemizedDelta(seconds=0).format_iso() 'PT0S' >>> ItemizedDelta(nanoseconds=0).format_iso() 'PT0.0S' >>> dict(ItemizedDelta.parse_iso("PT0.0S")) {'seconds': 0, 'nanoseconds': 0}
Component-wise composition carries and borrows between
secondsandnanoseconds, and between those two only:>>> ItemizedDelta(nanoseconds=999_999_999).add(nanoseconds=1) ItemizedDelta("PT1.0s")
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:
>>> 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:
>>> TimeDelta(hours=1, minutes=90) == TimeDelta(hours=2, minutes=30)
True # normalized deltas are the same
The two itemized types compare equal when their components are, so a date delta equals the same delta with its exact components zero or absent:
>>> ItemizedDelta(days=3, hours=0) == ItemizedDateDelta(days=3)
True
Use strict_eq() when explicit component presence
matters (see Strict equality); it takes only a delta of its own type,
and raises TypeError for the other. hash() follows ==, so an
explicit zero hashes like a missing component, and equal deltas of the two
types hash alike.
Constructors currently require at least one component, so construct an
itemized zero with an explicit component such as ItemizedDelta(seconds=0) or
ItemizedDateDelta(days=0). Allowing empty constructors may be considered as
an additive change after 1.0.
Sign¶
All delta types carry a single sign that applies to every component uniformly—there are no mixed-sign deltas.
>>> 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:
>>> d = ItemizedDelta(hours=2, minutes=30)
>>> -d
ItemizedDelta("-PT2h30m")
TimeDelta also has a single sign, but may be constructed
with mixed-sign components, as they will be normalized into a single sign automatically:
>>> 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 (and its itemized-delta equivalents).
This is sometimes called “balancing”—redistributing the value
across the requested units:
>>> 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:
>>> hours, minutes = TimeDelta(minutes=150).in_units(["hours", "minutes"]).values()
(2, 30)
Rounding applies to the smallest unit and carries into the larger ones:
23.5 hours rounded up in days and hours is 1 day and 0 hours, never 24 hours.
With round_increment=, the smallest unit is a multiple of the increment,
and rounding it up to the next unit carries: 5 hours 20 minutes in hours and
minutes with an increment of 90 is PT5h0m, or PT6h0m with
round_mode="ceil".
"nanoseconds" is accepted only together with "seconds":
the component is bounded, so it cannot hold a difference on its own
(see Seconds and nanoseconds).
Tip
If you need the difference between two datetimes in specific units,
use since() / until()
instead of computing a delta and converting it.
See 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 (and its itemized-delta equivalents), which
returns a float, or an int for "nanoseconds".
>>> 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. total() also accepts
"milliseconds" and "microseconds", which have no itemized component; see
Seconds and nanoseconds.
Note
For ItemizedDelta and 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 supports comparison operators
(such as >, <, >=, and <=),
as these operations only make sense when exclusively working with exact time units:
>>> TimeDelta(minutes=90) > TimeDelta(hours=1)
True
ItemizedDateDelta and 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.
>>> 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:
>>> 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() and subtract() methods.
These methods return a new delta representing the sum or difference
of the two deltas:
>>> TimeDelta(hours=2, minutes=30).add(hours=1)
TimeDelta("PT3h30m")
The itemized deltas compose component-wise: like components add up,
and the result stays itemized, so 1 month plus 30 days is
1 month 30 days whatever the month. To balance the result, call
in_units() on it with a reference:
>>> summed = ItemizedDateDelta(months=1).add(days=30, month_composition_ok=True)
>>> summed
ItemizedDateDelta("P1m30d")
>>> summed.in_units(["months", "days"], relative_to=Date(2023, 1, 1))
ItemizedDateDelta("P2m2d")
>>> summed.in_units(["months", "days"], relative_to=Date(2023, 2, 28))
ItemizedDateDelta("P1m30d")
The operands decide the result type: two ItemizedDateDelta
operands give an ItemizedDateDelta, and any ItemizedDelta
operand gives an ItemizedDelta.
Composing years or months emits MonthCompositionWarning,
which month_composition_ok=True accepts. A month clamps at the end of the
month, so the composed delta can land on a different day than the deltas
applied in turn; Composing deltas shows an example. Days, weeks, and
exact units never clamp, and compose without a warning.
Each reference type on in_units(),
total(), in_units(), and
total() has its own warning:
A
ZonedDateTimeresolves calendar units in its time zone and emits no warning.A
PlainDateTimeignores time zone transitions. It emitsNaiveArithmeticWarningwhen the computation crosses the calendar/exact boundary, whichnaive_arithmetic_ok=Trueaccepts.An
OffsetDateTimeholds its offset fixed for the whole calculation. It emitsStaleOffsetWarningwhen calendar units are involved, whichstale_offset_ok=Trueaccepts.
An ItemizedDateDelta reads only the date of a datetime reference,
without a warning.
Operators¶
TimeDelta follows the numeric protocol, as
timedelta does: *, /, //, %, and unary + exist
on it alone, since scaling only makes sense for one exact duration.
Multiplying or dividing by a number rounds half-even to the nearest
nanosecond; an integer operand is exact, a float operand carries float
precision. A float keyword to the constructor or add() is multiplied
into nanoseconds in double precision, then truncated toward zero:
TimeDelta(seconds=1.5e-9) is 1 nanosecond, where
TimeDelta(seconds=1) * 1.5e-9 rounds to 2. Dividing by another TimeDelta gives a float; // and %
take a TimeDelta divisor only.
>>> delta = TimeDelta(hours=2, minutes=30)
>>> delta * 2
TimeDelta("PT5h")
>>> delta / 2
TimeDelta("PT1h15m")
>>> delta / TimeDelta(minutes=30)
5.0
>>> delta // TimeDelta(hours=1), delta % TimeDelta(hours=1)
(2, TimeDelta("PT30m"))
The itemized deltas are mappings, not numbers: they have +, -, unary
-, and abs(). The binary operators compose component-wise, with the
same MonthCompositionWarning as the methods; only the
methods take month_composition_ok=True. An itemized
delta has one sign, so a composition that leaves components of both signs
raises ValueError: ItemizedDelta(hours=1) + ItemizedDelta(minutes=-90)
is rejected with “mixed sign in delta”. To split a delta into its date and
time halves, use date_and_time_parts().
sign() exists where ordering does not: the itemized deltas cannot be
compared, so it is how you read their sign, whereas a TimeDelta compares
with TimeDelta.ZERO.
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. Applying one delta composes nothing, so
these operations never emit MonthCompositionWarning.
As with the equivalent add() and subtract() methods, calendar units are
applied before exact units, and years and months clamp; so adding and then
subtracting the same delta is not always reversible.
Rounding¶
Only TimeDelta has a round() method for rounding to a specific unit:
>>> 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() method:
>>> 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 and boundaries for more information on rounding modes and increments.
ISO 8601 format¶
The ISO 8601 standard defines formats for specifying durations, the most common being:
±P nY nM nW nD T nH nM nS (spaces added for clarity)
Where:
Pis the period designator, andTseparates date and time components.nYis the number of years,nMis the number of months,nWthe number of weeks, etc.Only seconds may have a fractional part.
At least one component must be present (it may be zero).
For example:
P3Y4DT12H30Mis 3 years, 4 days, 12 hours, and 30 minutes.-P2M5Dis -2 months, and -5 days.P0Dis zero for the itemized deltas.TimeDelta.parse_iso()rejects it, withP1DandP1W, since days and weeks are calendar units.+PT5M4.25Sis 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(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")
Why not support the full ISO 8601 standard?
Full conformance to the ISO 8601 standard is not provided, because:
It allows for a lot of unnecessary flexibility (e.g. fractional components other than seconds)
There are different revisions with different rules
The full specification is not freely available
Supporting a commonly used subset is more practical. This is also what all established libraries do.
Equivalents in other languages¶
The three delta types in whenever are similar to those in other languages:
Library |
|||
|---|---|---|---|
NodaTime (C#) |
|
|
|
java.time (Java) |
|
|
|
Jiff (Rust) |
|
|
|
Temporal (JS) |
|