Skip to content

Handle integers larger than a float in apnumber and fractional - #420

Open
itzzdev09 wants to merge 1 commit into
python-humanize:mainfrom
itzzdev09:fix/overflow-apnumber-fractional
Open

itzzdev09 wants to merge 1 commit into
python-humanize:mainfrom
itzzdev09:fix/overflow-apnumber-fractional

Conversation

@itzzdev09

Copy link
Copy Markdown

Fixes #419

Changes proposed in this pull request:

  • apnumber now tries int() before float(), so an integer larger than the float range no longer raises OverflowError
  • fractional distinguishes a genuinely infinite value from an integer that float() merely reports as infinite, so a finite integer is no longer rendered as +Inf
  • Tests covering both, as int and as str, plus the "1e400" case that must still be +Inf

Both functions converted the input with float() purely to test for NaN and infinity, before doing the int() conversion they actually needed. For an integer beyond the float range that raises OverflowError, which is not in the except (TypeError, ValueError) clause, so it reached the caller instead of the documented fallback:

>>> humanize.apnumber(10**400)
OverflowError: int too large to convert to float
>>> humanize.fractional(10**400)
OverflowError: int too large to convert to float

apnumber documents the intended behaviour explicitly — "always returns a string unless the value was not int-able, then str(value) is returned" — and 10**400 is int-able, so this is a contract violation rather than unsupported input.

The string side had the mirror-image problem. float("1" + "0"*400) returns inf rather than raising, so a finite integer was reported as +Inf. fractional still needs a float, so it keeps that conversion but now only treats an infinite result as genuine when the value isn't itself an integer. An integer that large has no fractional part to extract either way.

Behaviour after the change:

input apnumber fractional
10**400 digits digits
"1" + "0"*400 digits digits
-(10**400) digits digits
"1e400" +Inf +Inf
inf / -inf / nan +Inf / -Inf / NaN same
0, 5, 1.0, "5.0" zero, five, one, 5.0 unchanged
0.5, 1.5, "1/3" unchanged 1/2, 1 1/2, 1/3

"1e400" deliberately still reports +Inf: it isn't an integer, so it is genuinely infinite once parsed.

Testing. pytest tests/test_number.py gives 249 passed, and the full suite 737 passed / 112 skipped. I reverted number.py while keeping the new tests and confirmed all six fail with OverflowError, so they are genuine regression coverage rather than decoration. (The 15 errors I see in tests/test_benchmarks.py are pytest-benchmark not being installed locally, and occur on main too.)

Scope. #419 lists the same pattern in intword, scientific, clamp and metric. ordinal, intcomma and intword are already covered by #414, #404 and #415/#332, so this PR deliberately avoids them to prevent conflicts. scientific is left out on purpose — a str(value) fallback there would emit 401 digits from a function whose whole point is compact notation, so it likely wants Decimal-based formatting (1.00 x 10⁴⁰⁰) instead. Happy to follow up with that separately if the approach sounds right.

Both functions converted the input with float() before trying int(),
only to test for NaN and infinity. For an integer beyond the float
range that conversion raises OverflowError, which is not in the
except clause, so it reached the caller instead of the documented
str(value) fallback -- despite the value being an ordinary int.

apnumber now tries int() first and falls back to float() only for
values that are not int-able, mirroring the fix applied to ordinal.

fractional still needs a float, so it keeps that conversion but now
treats an infinite result as genuine only when the value is not
itself an integer: float("1" + "0" * 400) returns inf rather than
raising, which previously reported a finite integer as "+Inf". An
integer that large has no fractional part to extract either way.

A literal "1e400" is still reported as +Inf, since it is not an
integer.
@Metis-dot

Copy link
Copy Markdown

Automated AI review. Reproduced on humanize 4.16.0 and current main; the remaining PR behavior is inferred from the diff, not a run of this branch.

One edge still appears uncovered: finite built-in integers above binary64’s exact-integer range but below float overflow. On current main, humanize.fractional(2**53 + 1) returns "9007199254740992" instead of "9007199254740993" (and the negative input loses the same low bit). The PR’s fractional path still sends these finite values through float(value), so they round silently. Could an exact built-in-int fast path before the float conversion, with positive and negative 2**53 + 1 regression cases, fit this fix?

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.

Several number functions raise OverflowError on integers larger than a float

2 participants