Issue 33002: Making a class formattable as hex/oct integer with printf-style formatting requires both int and index for no good reason (original) (raw)

In Python 2, making a user-defined class support formatting using the integer-specific type codes required that int be defined and nothing else (that is, '%x' % Foo() only required Foo to provide a int method). In Python 3, this was changed to perform the conversion via index for the %o, %x and %X format types (to match how oct and hex behave), not int, but the pre-check for validity in unicodeobject.c's mainformatlong function is still based on PyNumber_Check, not PyIndex_Check, and PyNumber_Check is concerned solely with int and float, not index.

This means that a class with index but not int can't be used with the %o/%x/%X format codes (even though hex(mytype) and oct(mytype) work just fine).

It seems to me that either:

  1. PyNumber_Check should be a superset of PyIndex_Check (broader change, probably out of scope)

or

  1. mainformatlong should restrict the scope of the PyNumber_Check test to only being used for the non-'o'/'x'/'X' tests (where it's needed to avoid coercing strings and the like to integer).

Change #2 should be safe, with no major side-effects; since PyLong and subclasses always passed the existing PyNumber_Check test anyway, and PyNumber_Index already performs PyIndex_Check, the only path that needs PyNumber_Check is the one that ends in calling PyNumber_Long.

Note: Obviously, defining index without defining int is a little strange (it's equivalent to int, but can't be coerced to int?), so yet another fix would be addressing #20092 so it wouldn't be possible for a type to define index without (implicitly) defining int.

To be clear, this is a problem with old-style (printf-style) formatting, and applies to both bytes formatting and str formatting. So a class like:

class Foo:
    def __index__(self):
        return 1

will fail with a TypeError should you do any of:

'%o' % Foo()
'%x' % Foo()
'%X' % Foo()
b'%o' % Foo()
b'%x' % Foo()
b'%X' % Foo()

even though hex(Foo()) and oct(Foo()) work without issue.