Skip to content

Fix Duration multiplication by a float dropping years and months#975

Open
gaoflow wants to merge 1 commit into
python-pendulum:masterfrom
gaoflow:fix-duration-mul-float
Open

Fix Duration multiplication by a float dropping years and months#975
gaoflow wants to merge 1 commit into
python-pendulum:masterfrom
gaoflow:fix-duration-mul-float

Conversation

@gaoflow

@gaoflow gaoflow commented Jun 29, 2026

Copy link
Copy Markdown

The bug

Multiplying a Duration by a float silently drops the years and months components, even though multiplying by an int — and dividing by a float — both keep them:

>>> import pendulum
>>> pendulum.duration(years=1) * 2       # int
Duration(years=2)
>>> pendulum.duration(years=1) * 2.0     # float
Duration()                               # ← years gone
>>> pendulum.duration(years=1) / 2.0     # float division keeps them
Duration(years=1) ...

Duration(years=2, months=4) * 2.0 returns an empty Duration(). Since * 2 and * 2.0 are mathematically the same operation, this is a clear inconsistency.

Root cause

Duration.__mul__ (src/pendulum/duration.py) builds the float result solely from _to_microseconds(), which by design excludes years/months, and hardcodes them to 0:

if isinstance(other, float):
    usec = self._to_microseconds()
    a, b = other.as_integer_ratio()
    return self.__class__(0, 0, _divide_and_round(usec * a, b))   # years/months dropped

__truediv__ and __floordiv__ already handle this correctly by scaling years/months alongside the microseconds.

Fix

Scale years and months by the float ratio too, mirroring the existing __truediv__ float branch:

return self.__class__(
    0,
    0,
    _divide_and_round(usec * a, b),
    years=_divide_and_round(self._years * a, b),
    months=_divide_and_round(self._months * a, b),
)

Now duration(years=1) * 2.0 == duration(years=1) * 2, and a non-integer factor rounds years/months the same way float division already does.

Tests

Added test_multiply_float (mirrors the existing test_divide float case): a float factor matches the integer result, is commutative (2.0 * it), a whole-valued float equals the int product exactly, and a non-integer factor rounds years/months. The new test fails on master (years 0 != 4) and passes with the fix; the full tests/duration suite stays green. ruff and mypy clean.

Duration.__mul__ with a float built the result only from
_to_microseconds(), which excludes the years and months components, so
Duration(years=1) * 2.0 returned an empty Duration even though
Duration(years=1) * 2 (int) and Duration(years=1) / 2.0 (float) both keep
them. Scale years and months by the float ratio as well, mirroring the
existing __truediv__ implementation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant