When is __init__ called exactly?

I am trying to understand what is happening in the following code. According to the documentation, X.__init__ is called with the arguments passed to X(), but that doesn’t seem to happen here, or there is some magic going on. X doesn’t define __init__, so object.__init__() should be called, but that isn’t happening because X(1,2,3) just works. For comparison, Y works as expected, if you pass something to Y() then object.__init__ complains that it was given unexpected arguments. Does the presence of __new__ together with the absence of __init__ mean something special?

class X:
    def __new__(cls, *args, **kwargs):
        return super().__new__(cls)

class Y:
    def __new__(cls, *args, **kwargs):
        return super().__new__(cls)
    def __init__(self, *args, **kwargs):
        return super().__init__(*args, **kwargs)
>>> X(1,2,3)
<__main__.X object at 0x000001CCD54AEDC0>
>>> Y(1,2,3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in __init__
TypeError: object.__init__() takes exactly one argument (the instance to initialize)

There are two methods involved during class instantiation, namely; “new” and “init”.
new” is a static method and it’s called first with the first argument being the class with which a new instance is to be created. After an instance is created by “new” , the process calls “init” with the rest of the arguments that were passed to “new” in order to initialize and return a full instance.

All classes are subclasses of the Object class in Python, and the Object class declares “dunder” methods that can be overriden, by virtual of X being a default subclass of object class, it means it already has a default “init” method declared, and that’s why it just works when called. The “dunder” methods still have to remain in the bounds of the conventions in which they are used. Firstly, an “init” function is not supposed to return anything.
Secondly, you’ve called the “super().init” method and passed in *args and **kwargs, which calls the “init” method from the Object class since it’s the direct super class of Y, and that “init” function only expects one argument, which is the return value of “new”, and that’s why it’s raising an error.
So, a better way to have implemented Y is this

class Y:
    @staticmethod
    def __new__ (cls, *args, **kwargs):
        return object.__new__(cls)

    def __init__ (self, *args, **kwargs):
        #do something with the arguments
        print (args, kwargs)

My question isn’t about Y, it’s about X: why doesn’t X(1,2,3) raise an exception? It doesn’t call self.__init__(1,2,3) because that would be the object.__init__, which doesn’t accept extra arguments. So is __init__ called here, and if yes then when?

I’ve edited my answer above to kinda answer that. The actual reason why X works is because it is incomplete. It creates an instance but doesn’t call the initializer because it doesn’t have any. So it return an uninitialized instance and discards the values passed to it which would have been passed to init if it had one.

X is complete, it has __init__ method inherited from object. Anyway, here’s the same thing but with the base class which isn’t object:

>>> class A:
...     def __init__(self):
...         print("A.__init__")
...         super().__init__()
...
>>> class B(A):
...     def __new__(cls, *args):
...         return super().__new__(cls)
...
>>> B()
A.__init__
<__main__.B object at 0x000001CCD54C36D0>
>>> B(1,2,3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() takes 1 positional argument but 4 were given

B is quite like X, except its base is A and not object. But both A and object have __init__ which takes no extra parameters. It feels like object.__init__ is special, and it’s not actually called in some cases (like in the case of X).

Ok, it’s simple: cpython/typeobject.c at main · python/cpython · GitHub

object.__init__() is called for X but it doesn’t complain about extra arguments because X doesn’t override __init__. I suppose it’s done this way because reasons.

2 Likes

Hi TobiasHT,

You don’t need to decorate __new__ with staticmethod, it is
automatically done by the compiler. The __new__ method is special and
always converted to a static method.

3. Data model — Python 3.10.1 documentation.new

1 Like

Oh yeah, I get that. But when I’m writing code that is going to be used or worked upon by other Devs, I usually like being detailed in such a way.
If I’m working on something of my own, I would leave all that out.

It’s just a force of habit :sweat_smile:

What that code does is permit and ignore any arguments, if object.__init__ gets called on a subclass. This is done for your convenience, so you don’t need to override __init__ if you’re only wanting to do things in __new__ or vice versa.

1 Like

Tobias. To have double underscores displayed and quoted correctly, use a single backtic like so, `__new__`, and the result will be correct, __new__. (Note that I added extra escaped backtics to make the text look like what you should type.)

1 Like

Thanks for the tip. Actually, I wrote that all on my phone, kinda like how I’m doing with this one, of which the phone was displaying one screen. I had no idea that my double underscores were turning bold until I posted. I did have a feeling that the underscores would turn my comments bold, so I applied the quotes and assumed it would neutralize the effect, guess I thought wrong. Thanks though, we learn something new every day :blush: