Skip to content

Fix SystemError when convert()'ing an out-of-range int to float - #1162

Merged
Siyet merged 1 commit into
msgspec:mainfrom
STiFLeR7:fix/convert-float-overflow-systemerror
Sep 7, 2026
Merged

Siyet merged 1 commit into
msgspec:mainfrom
STiFLeR7:fix/convert-float-overflow-systemerror

Conversation

@STiFLeR7

Copy link
Copy Markdown
Contributor

Summary

msgspec.convert(obj, float) converts a Python int to a C double via PyLong_AsDouble without checking for overflow. For an int too large to represent as a finite float (e.g. 10**400), PyLong_AsDouble sets OverflowError internally but still returns -1.0, and that value was passed straight through to ms_decode_float and returned to the interpreter with the exception still set — which surfaces as:

SystemError: <built-in function convert> returned a result with an exception set

This bypasses the documented ValidationError contract entirely, so a caller wrapping the call in except msgspec.ValidationError doesn't catch it.

json.decode already handles the equivalent case correctly, raising ValidationError: Number out of range. This PR applies the same "check PyErr_Occurred() after a lossy C conversion, then report via ms_error_with_path" pattern already used elsewhere in this file (e.g. _constr_as_f64), so convert() reports the overflow the same way — for both bare (convert(big, float)) and nested (convert({"x": big}, dict[str, float])) targets, under both strict=True and strict=False.

Fixes #1122.

Testing

  • Reverted the fix locally and reproduced the exact reported SystemError before reapplying it.
  • Added test_float_from_int_out_of_range to TestFloat in tests/unit/test_convert.py, covering bare and nested targets under both strict modes.
  • Ran the full tests/unit/ suite: 6052 passed, 473 skipped, 0 failures.

msgspec.convert(obj, float) converts a python int to a C double via
PyLong_AsDouble without checking for overflow. For an int too large to
represent as a float, PyLong_AsDouble sets OverflowError but still
returns -1.0, and that value was passed straight through to
ms_decode_float and returned to the interpreter with the exception
still set - which surfaces as SystemError: <built-in function convert>
returned a result with an exception set, bypassing the documented
ValidationError contract entirely.

json.decode already handles the equivalent case correctly, raising
ValidationError: Number out of range. Apply the same
value-error-with-path check convert() already uses elsewhere in this
file (e.g. _constr_as_f64) so the overflow is reported the same way,
for both bare and nested (e.g. dict[str, float]) targets, under both
strict and lax mode.

Fixes msgspec#1122.

@Siyet Siyet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verified locally with current main (7c2f473) merged into this branch, Python 3.14.6, -X dev:

  • convert(10**400, float), the nested dict[str, float], tuple[float, ...] and Struct-field cases, strict=False, and the negative value all raise ValidationError: Number out of range with the right path suffix ($[...], $[1], $.x).
  • 2**1024 is rejected, 2**1023 and int(sys.float_info.max) still convert.
  • The OverflowError that PyLong_AsDouble sets does not leak: __context__ and __cause__ are None, sys.exc_info() is clean afterwards.
  • Full unit suite: 6387 passed, 143 skipped.

The check mirrors what _constr_as_f64 already does a few thousand lines up, and the message matches the JSON path. Bug fix with no API change, so I will put it in the merge queue once the CI runs (just approved for the first-time-contributor gate) come back green. The changelog entry gets added in the release-prep batch, same as the other fixes.

@codspeed

codspeed Bot commented Sep 5, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 139 untouched benchmarks
⏩ 135 skipped benchmarks1


Comparing STiFLeR7:fix/convert-float-overflow-systemerror (5be57b7) with main (f51f378)

Open in CodSpeed

Footnotes

  1. 135 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@Siyet

Siyet commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Thanks @STiFLeR7, merging this. It matches the pattern _core.c already uses for exactly this situation (the integer struct tag check and the Ext.code check both set the new error over the pending one without an intervening PyErr_Clear()), and parametrising the test over strict was the right call.

Two notes from verifying it on top of current main, both worth having in the thread:

It fixes more than the SystemError. With a constrained target, the bad -1.0 used to reach the constraint check, so on main this returned a wrong but believable message rather than crashing:

>>> msgspec.convert(10**400, Annotated[float, msgspec.Meta(ge=0)])
ValidationError: Expected `float` >= 0.0

for a value that plainly satisfies ge=0. That now reports Number out of range as well. Worth a line in the test if you feel like adding one, since that half is currently unguarded against a future regression.

The guard is exactly sufficient, no more needed. PyLong_AsDouble returns exactly -1.0 on every error path and structurally cannot return +/-inf, so testing the sentinel plus PyErr_Occurred() catches everything. A differential run of 81835 ints against float() as the oracle, covering the whole DBL_MAX boundary region and including int subclasses and IntEnum, found no disagreement in either direction, and convert(-1, float) still returns -1.0 as it must.

Full unit suite on the merge with current main: 6387 passed, 143 skipped, with the new test failing on main as a regression test should.

@Siyet
Siyet added this pull request to the merge queue Sep 7, 2026
Merged via the queue into msgspec:main with commit 3f67b34 Sep 7, 2026
26 of 27 checks passed

This branch had an error being deployed

1 failed deployment
docs-preview — 5be57b77 Deployed Aug 20, 2026 by STiFLeR7 via Deploy preview #411
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.

convert leaks SystemError on an out-of-range int → float, where json.decode cleanly raises ValidationError

2 participants