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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant