Skip to content

bin(), hex() and oct() ignore __index__ #19714

Description

@jseop-lim

Port, board and/or hardware

Unix port, macOS arm64

MicroPython version

MicroPython v1.30.0-preview.66.gcc12057519

Reproduction

class A:
    def __index__(self):
        return 2

print(bin(A()))

Expected behaviour

CPython prints (3.11.15):

0b10

Observed behaviour

MicroPython raises:

ValueError: unknown format code 'b' for object of type 'A'

hex(A()) and oct(A()) raise TypeError: can't convert A to int, while an object defining only __int__ is accepted by hex() and oct() where CPython raises TypeError.

Additional Information

The data model names __index__ as the method bin(), hex() and oct() use (docs). MicroPython's own difference tables list the related 3.8 and 3.10 changes without a status:

| Constructors of *int*, *float* and *complex* will now use the *__index__()* special method, if available | |
| and the corresponding method *__int__()*, *__float__()* or *__complex__()* is not available | |

| Builtin and extension functions that take integer arguments no longer accept | |
| :class:`~decimal.Decimal`\ s, :class:`~fractions.Fraction`\ s and other | |
| objects that can be converted to integers only with a loss (e.g. that have | |
| the :meth:`~object.__int__` method but do not have the | |
| :meth:`~object.__index__` method). | |

Root Cause

mp_builtin_bin (py/modbuiltins.c#L122-L125) passes its argument unconverted to '{:#b}'.format(), so the object lands in the formatter's default: arm and raises ValueError (py/objstr.c#L1607-L1613). The instance coercion used by hex() and oct() goes through MP_UNARY_OP_INT_MAYBE, which looks up only __int__ (py/objtype.c#L387), and __index__ is not looked up anywhere. CPython's bin() calls PyNumber_ToBase, which converts through _PyNumber_Index and calls nb_index.

Fix Suggestion

Map a new MP_UNARY_OP_INDEX_MAYBE to __index__, try it before __int__ in mp_obj_get_int_maybe() (py/obj.c#L358-L373), and coerce the argument in mp_builtin_bin() before formatting. The extra qstr and unary op could sit behind a config option.

Code of Conduct

Yes, I agree

Activity

  1. jseop-lim commented on Sep 17, 2026

    @jseop-lim
    ContributorAuthor

    I don't think this can be fixed at zero cost. Looking up __index__ needs a new qstr, and __int__ can't be repurposed because it is a static qstr fixed for .mpy compatibility.

    I see two options and would like your opinion on which to take:

    1. Accept the missing __index__ support.
      a. Optionally, document it with a tests/cpydiff/ entry.
    2. Fix it in code and accept the size cost.
  2. dpgeorge commented on Sep 18, 2026

    @dpgeorge
    Member

    I don't think this is important to fix/implement for MicroPython, there doesn't seem to be any real motivation (real world use cases).

    So it would be option (1), accept this as a difference to CPython. I'm not even sure it needs documenting? If there are too many (unimportant) differences that are documented then it becomes more noise than useful docs.

  3. jseop-lim commented on Oct 6, 2026

    @jseop-lim
    ContributorAuthor

    Sorry for the slow reply. @dpgeorge

    I looked for real code that would be affected, using GitHub code search across MicroPython projects and drivers. I found no code that passes an object defining only __index__ to bin(), hex(), oct(), or uses it as a list index. So I agree there's no real motivation here, and I'm happy to go with option 1 and accept this as a difference to CPython.

    On documentation, the missing __index__ does show up in the docs, but only indirectly, in rows about other functions:

    • Python 3.8: int(), float() and complex() constructors fall back to __index__.
    • Python 3.10: functions taking integer arguments reject objects that only define __int__.

    Neither says that bin(), hex() and oct() ignore __index__, and the per-version pages can't really say it, since they track changes made after Python 3.4 while these three functions have used __index__ since Python 3.0. That leaves tests/cpydiff/ as the natural place, and a single entry there would cover all three functions.

    If you think that's worth adding, I'll open a PR for it. Otherwise feel free to close this issue as is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions