6

I am running into an issue with subtyping the str class because of the str.__call__ behavior I apparently do not understand.

This is best illustrated by the simplified code below.

class S(str):
    def __init__(self, s: str):
        assert isinstance(s, str)
        print(s)

class C:
    def __init__(self, s: str):
        self.s = S(s)

    def __str__(self):
        return self.s

c = C("a")  # -> prints "a"
c.__str__() # -> does not print "a"
str(c)      # -> asserts fails in debug mode, else prints "a" as well!?

I always thought the str(obj) function simply calls the obj.__str__ method, and that's it. But for some reason it also calls the __init__ function of S again. Can someone explain the behavior and how I can avoid that S.__init__ is called on the result of C.__str__ when using the str() function?

pfp.meijers
  • 774
  • 7
  • 7

1 Answers1

7

Strictly speaking, str isn't a function. It's a type. When you call str(c), Python goes through the normal procedure for generating an instance of a type, calling str.__new__(str, c) to create the object (or reuse an existing object), and then calling the __init__ method of the result to initialize it.

str.__new__(str, c) calls the C-level function PyObject_Str, which calls _PyObject_Str, which calls your __str__ method. The result is an instance of S, so it counts as a string, and _PyObject_Str decides this is good enough rather than trying to coerce an object with type(obj) is str out of the result. Thus, str.__new__(str, c) returns c.s.

Now we get to __init__. Since the argument to str was c, this also gets passed to __init__, so Python calls c.s.__init__(c). __init__ calls print(c), which you might think would call str(c) and lead to infinite recursion. However, the PRINT_ITEM opcode calls the C-level PyFile_WriteObject to write the object, and that calls PyObject_Str instead of str, so it skips the __init__ and doesn't recurse infinitely. Instead, it calls c.__str__() and prints the resulting S instance, as the S instance is a string.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • I think I got it. The `__init__` is part of the normal sequence behind a `__call__` (i.e. a `__new__` followed by a `__init__`). I didn't thought about that. Thanks for the explanation! – pfp.meijers Jan 18 '16 at 18:39
  • The fact that the `C` object is used as argument for the `__init__`, that's the real tricky/not-intuitive thing here. If I use annotations with run-time checks, and in case of failure print the object info via a `str()` call, then I have an infinite recursion. This case is actually the trigger for my question. Wondering how to workaround? Maybe always use `__str__` directly instead of a `str()`? – pfp.meijers Jan 18 '16 at 18:54
  • 1
    @pfp.meijers: Why do you even have this weird `str` subclass with its really weird `__init__`? I would recommend having your `__str__` return an object with `type(obj) is str`. If you really want to use this weird `str` subclass, you should probably have it implement `__new__` instead of `__init__`. – user2357112 Jan 18 '16 at 19:03
  • The `str` subclass is weird because I simplified it here for illustration purposes. But your suggestion to return a `type(obj) is str` will do the job. So that is how I will solve it. Thanks! – pfp.meijers Jan 18 '16 at 19:10