pfp.meijers
pfp.meijers

Reputation: 962

Unexpected behavior of python builtin str function

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?

Upvotes: 6

Views: 205

Answers (1)

user2357112
user2357112

Reputation: 281758

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.

Upvotes: 7

Related Questions