Skip to content

Correct IFDRational.__float__() return value#9676

Open
nyxst4ck wants to merge 1 commit into
python-pillow:mainfrom
nyxst4ck:fix/ifdrational-float
Open

Correct IFDRational.__float__() return value#9676
nyxst4ck wants to merge 1 commit into
python-pillow:mainfrom
nyxst4ck:fix/ifdrational-float

Conversation

@nyxst4ck

@nyxst4ck nyxst4ck commented Jun 16, 2026

Copy link
Copy Markdown

Changes proposed

IFDRational.__float__ returns the wrong value for a non-integral numerator.

IFDRational delegates __eq__, __int__, __round__, __repr__ and all arithmetic to self._val (the normalized Fraction), but never defined __float__. So float() falls back to numbers.Rational.__float__, which does int(self.numerator) / int(self.denominator). For a non-integral numerator the stored _numerator/_denominator are un-normalized, so the result disagrees with every other accessor:

>>> from PIL.TiffImagePlugin import IFDRational
>>> r = IFDRational(1.5, 3)   # value 1.5 / 3 == 0.5
>>> r == 0.5
True
>>> int(r), repr(r)
(0, '0.5')
>>> float(r)
0.3333333333333333          # int(1.5) / int(3) == 1/3, should be 0.5

This is reachable via the public IFDRational(value, denominator) constructor.

Fix

Delegate __float__ to self._val, the same way __int__, __round__, __repr__ and __eq__ are delegated, so float() is consistent with the normalized fraction.

Tests

Added test_float to Tests/test_tiff_ifdrational.py: it asserts float(IFDRational(1.5, 3)) == 0.5, and that the integral and 0/0 (nan) cases stay correct. It fails on main (0.333… != 0.5) and passes with the change; Tests/test_tiff_ifdrational.py is green (6 passed). ruff and black clean.

IFDRational delegates __eq__, __int__, __round__, __repr__ and arithmetic to
self._val (the normalized Fraction), but never defined __float__. For a
non-integral numerator (e.g. IFDRational(1.5, 3), whose value is 0.5) float()
fell back to numbers.Rational.__float__, which computes
int(numerator) / int(denominator) = int(1.5) / int(3) = 0.333..., disagreeing
with ==, int() and repr() (all 0.5).

Delegate __float__ to self._val so float() is consistent with the rest of the
class.
Comment on lines +44 to +46
# normalized fraction. For a non-integral numerator (1.5 / 3 == 0.5) the
# inherited Rational.__float__ used int(numerator) / int(denominator) and
# returned 1/3 instead.

@radarhere radarhere Jun 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# normalized fraction. For a non-integral numerator (1.5 / 3 == 0.5) the
# inherited Rational.__float__ used int(numerator) / int(denominator) and
# returned 1/3 instead.
# normalized fraction

I don't think we need to document prior incorrect behaviour.

assert r == 0.5
assert float(r) == 0.5

# Integral and 0/0 (nan) cases stay correct.

@radarhere radarhere Jun 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Integral and 0/0 (nan) cases stay correct.
# Integer numerator and 0/0 (nan) cases

@radarhere radarhere changed the title Fix IFDRational.__float__ returning the wrong value Correct IFDRational.__float__() return value Jun 17, 2026
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.

2 participants