Skip to content

py: Support converting int subclass instances to float. - #19904

Open
0xABCD01 wants to merge 2 commits into
micropython:masterfrom
0xABCD01:fix/19874-int-subclass-float
Open

0xABCD01 wants to merge 2 commits into
micropython:masterfrom
0xABCD01:fix/19874-int-subclass-float

Conversation

@0xABCD01

@0xABCD01 0xABCD01 commented Nov 7, 2026

Copy link
Copy Markdown

Fixes #19874.

float() raises TypeError: can't convert MyInt to float for instances of an int subclass. complex(), math.sqrt(), '%f' formatting and arithmetic with a float operand fail the same way. CPython accepts all of these.

An int subclass instance is a generic instance object wrapping the int value, so the float conversion path does not recognise it: the FLOAT_MAYBE unary op is not implemented for the wrapped int, while the matching INT_MAYBE op is — which is why int() already works on the same objects.

This change implements MP_UNARY_OP_FLOAT_MAYBE next to the existing MP_UNARY_OP_INT_MAYBE cases in py/runtime.c (small ints), py/objint_mpz.c and py/objint_longlong.c. A subclass that defines its own __float__ still takes precedence, because conversion still goes through the unary-op dispatch on the instance first.

Tests: new tests in tests/float/ (int_subclass_convert.py, int_subclass_convert_intbig.py, complex_int_subclass.py). run-tests results: basics 552/552, float 72/72, extmod 190/190, micropython 104/104, misc 7/7, cmdline 25/25.

Disclosure: I used AI assistance to investigate this issue and prepare this change, and I reviewed the diff and test results before submitting.

@codecov

codecov Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.60%. Comparing base (75e9d71) to head (627b834).
⚠️ Report is 3 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #19904      +/-   ##
==========================================
+ Coverage   98.57%   98.60%   +0.03%     
==========================================
  Files         182      182              
  Lines       23378    23380       +2     
  Branches        5        5              
==========================================
+ Hits        23044    23054      +10     
+ Misses        333      325       -8     
  Partials        1        1              
Flag Coverage Δ
unix-coverage-32bit 98.60% <100.00%> (+0.03%) ⬆️
unix-coverage-64bit 98.53% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Code size report:

Reference:  pic16bit: Add README about port tier, not actively maintained. [c1c4e7b]
Comparison: tests/float: Add tests for int subclass instances converting to float. [merge of 627b834]
  mpy-cross:   +52 +0.014% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:   +72 +0.008% standard
      stm32:   +32 +0.008% PYBV10
      esp32:   +40 +0.002% ESP32_GENERIC
     mimxrt:   +32 +0.008% TEENSY40
        rp2:   +32 +0.003% RPI_PICO_W
       samd:   +32 +0.011% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:   +44 +0.010% VIRT_RV32

@0xABCD01
0xABCD01 force-pushed the fix/19874-int-subclass-float branch from 0eae561 to 9024ae7 Compare November 7, 2026 12:05
@dpgeorge dpgeorge added the py-core Relates to py/ directory in source label Nov 8, 2026
@Josverl

Josverl commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

There appears to be some room left for optimization

A quick experiment

diff --git a/py/objint_longlong.c b/py/objint_longlong.c
--- a/py/objint_longlong.c
+++ b/py/objint_longlong.c
@@ -107,10 +107,6 @@ mp_obj_t mp_obj_int_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
         }
         case MP_UNARY_OP_INT_MAYBE:
             return o_in;
-        #if MICROPY_PY_BUILTINS_FLOAT
-        case MP_UNARY_OP_FLOAT_MAYBE:
-            return mp_obj_new_float(mp_obj_int_as_float_impl(o_in));
-        #endif
         default:
             return MP_OBJ_NULL;      // op not supported
     }
diff --git a/py/objint_mpz.c b/py/objint_mpz.c
--- a/py/objint_mpz.c
+++ b/py/objint_mpz.c
@@ -161,10 +161,6 @@ mp_obj_t mp_obj_int_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
         }
         case MP_UNARY_OP_INT_MAYBE:
             return o_in;
-        #if MICROPY_PY_BUILTINS_FLOAT
-        case MP_UNARY_OP_FLOAT_MAYBE:
-            return mp_obj_new_float(mp_obj_int_as_float_impl(o_in));
-        #endif
         default:
             return MP_OBJ_NULL;      // op not supported
     }
diff --git a/py/objtype.c b/py/objtype.c
--- a/py/objtype.c
+++ b/py/objtype.c
@@ -433,6 +433,11 @@ static mp_obj_t instance_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
     };
     mp_obj_class_lookup(&lookup, self->base.type);
     if (member[0] == MP_OBJ_SENTINEL) {
+        #if MICROPY_PY_BUILTINS_FLOAT
+        if (op == MP_UNARY_OP_FLOAT_MAYBE && mp_obj_is_int(self->subobj[0])) {
+            return mp_obj_new_float(mp_obj_get_float(self->subobj[0]));
+        }
+        #endif
         return mp_unary_op(op, self->subobj[0]);
     } else if (member[0] != MP_OBJ_NULL) {
         mp_obj_t val = mp_call_function_1(member[0], self_in);
diff --git a/py/runtime.c b/py/runtime.c
--- a/py/runtime.c
+++ b/py/runtime.c
@@ -314,10 +314,6 @@ mp_obj_t mp_unary_op(mp_unary_op_t op, mp_obj_t arg) {
             case MP_UNARY_OP_POSITIVE:
             case MP_UNARY_OP_INT_MAYBE:
                 return arg;
-            #if MICROPY_PY_BUILTINS_FLOAT
-            case MP_UNARY_OP_FLOAT_MAYBE:
-                return mp_obj_new_float((mp_float_t)val);
-            #endif
             case MP_UNARY_OP_NEGATIVE:
                 // check for overflow
                 if (val == MP_SMALL_INT_MIN) {

Cost ~48 bytes on the Unix port.

Also the changed longlong backend — int_subclass_convert_intbig.py:2 requires arbitrary-precision integers, so it skips MICROPY_LONGINT_IMPL_LONGLONG. I would suggest to add an int-64 test using positive and negative heap integers

I usually prefer the code in a seperate commit from the tests,
And please avoid referencing an issue in the commit message as that causes additional noise in GitHub.
(Please don't ask how I know)

float() on an instance of an int subclass raised TypeError, because
mp_obj_get_float_maybe() only recognises small ints and objects of
exact type int, and its MP_UNARY_OP_FLOAT_MAYBE fallback could not
resolve for a subclass instance: instance_unary_op() delegates the
op to the instance's native sub-object, but nothing converted the
sub-object when it was an int.

Handle MP_UNARY_OP_FLOAT_MAYBE in instance_unary_op(), at the point
where the op is delegated to the native sub-object: if that
sub-object is an int, convert it directly.  A __float__ method
defined by the subclass still takes precedence, because the member
lookup finds it before this delegation happens.  This also fixes
complex(), the math module functions, float arithmetic and %
formatting with int subclass instances, which all convert through
mp_obj_get_float_maybe().

Signed-off-by: Hafiar <[email protected]>
Covers float(), complex(), math functions, string formatting and
float arithmetic with int subclass instances, for small-int values,
big-int values, and int64-range heap int values (the latter also
runs with the longlong long-int implementation).

Signed-off-by: Hafiar <[email protected]>
@0xABCD01
0xABCD01 force-pushed the fix/19874-int-subclass-float branch from 9024ae7 to 627b834 Compare November 9, 2026 04:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

py-core Relates to py/ directory in source

Projects

None yet

Development

Successfully merging this pull request may close these issues.

float() rejects instances of an int subclass

4 participants