Python: __init__ is NOT a constructor: a deep dive in Python object creation

Author:Murphy  |  View: 24931  |  Time: 2025-03-22 23:57:19

Did you know that the __init__ method is not a constructor? But if __init__ doesn't create the object, then what does? How do objects get created in Python? Does Python even have a constructor?

The goal of this article is to better understand how Python creates objects and manipulate this process to make better appplications.

First we'll take a deep dive in how Python creates objects. Next we'll apply this knowledge and discuss some interesting use cases with some practical examples. Let's code!


1. Theory: Creating objects in Python

In this part we'll figure out what Python does under the hood when you create an object. In the next part we'll take this new knowledge and apply in in part 2.

How to create an object in Python?

This should be pretty simple; you just create an instance of a class. Alternatively you could create a new built-in type like a str or an int. In the code below an instance is created of a basic class. It just contains an __init__ function and a say_hello method:

class SimpleObject:
  greet_name:str

  def __init__(self, name:str):
    self.greet_name = name

  def say_hello(self) -> None:
    print(f"Hello {self.greet_name}!")

my_instance = SimpleObject(name="bob")
my_instance.say_hello()

Notice the __init__ method. It receives a name parameter and stores its value on the greet_name attribute of the SimpleObject instance. This allows our instance to keep state.

Now the question rises: in order to save the state, we need to have something to save the state on. Where does __init__ get the object from?


So, is init a constructor?

The answer: technically no. Constructors actually create the new object; the __init__ method is only responsible for setting the state of the object. It just receives values through it's parameters and assigns them to the class attributes like greet_name.

In Python, the actual creation of an object right before initialization. For object creation, Python uses a method called __new__ that's present on each object.

Creating and Publishing Your Own Python Package for Absolute Beginners


What does new do?

__new__ is a class method, meaning it is called on the class itself, not on an instance of the class. It is present on each object and is responsible for actually creating and returning the object. The most important aspect of __new__ is that it must return an instance of the class. We'll tinker with this method later in this article.


Where does the new method come from?

The short answer: everything in Python is an object, and the object class has a __new__ method. You can think of this as "each class inherits from the object class".

Notice that even though our SimpleObject class inherit from anything, we can still proof that it's an instance of object:

# SimpleObject is of type 'object'
my_instance = SimpleObject(name="bob")
print(isinstance(my_instance, object))    # <-- True
# but all other types as well:
print(isinstance(42, object))             # <-- True
print(isinstance('hello world', object))  # <-- True
print(isinstance({"my": "dict"}, object)) # <-- True

In summary, everything is an object, object defines a __new__ method thus everything in Python has a __new__ method.


How does new differ from init?

The __new__ method is used for actually creating the object: allocating memory and returning the new object. Once the object is created we can initialize it with __init__; setting up the initial state.

Python args, kwargs, and All Other Ways to Pass Arguments to Your Function


What does Python's process of object creation look like?

Internally, the functions below get executed when you create a new object:

  • __new__: allocates memory and returns the new object
  • __init__: initialize newly created object; set state

In the code below we demonstrate this by overriding __new__ with our own function. In the next part we'll use this principle to do some interesting things:

class SimpleObject:
  greet_name:str

  def __new__(cls, *args, **kwargs):      # <-- newly added function
    print("__new__ method")               
    return super().__new__(cls)            

  def __init__(self, name:str):
    print("__init__ method")
    self.greet_name = name

  def say_hello(self) -> None:
    print(f"Hello {self.greet_name}!")

my_instance = SimpleObject(name="bob")
my_instance.say_hello()

(_we'll explain why and how this code works in the next parts)._This will print the following:

__new__ method
__init__ method
Hello bob!

This means that we have access to the function that initialized an instance of our class! We also see that __new__ executes first. In the next part we'll understand the behaviour of __new__: what does super().__new__(cls) mean?


How does __new__ work?

The default behaviour of __new__ looks like the code below. In this part we'll try to understand what's going on so that we tinker with it in the practical examples in the next part.

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

Notice that __new__ is being called on the super() method, which returns a "reference" (it's actually a proxy-object) to the parent-class of SimpleObject. Remember that SimpleObject inherits from object, where the __new__method is defined.

Breaking it down:

  1. we get a "reference" to the base class of the class we're in. In the case of SimpleObject we get a "reference" to object
  2. We call __new__ on the "reference" so object.__new__
  3. We pass in cls as an argument. This is how class methods like __new__ work; it's a reference to the class itself

Putting it all together: we ask SimpleObject‘s parent-class to create a new instance of SimpleObject. This is the same as my = object.__new__(SimpleObject)


Can I then also create a new instance using new?

Yes, remember that the default __new__ implementation actually calls it directly: return super().new(cls). So the approaches in the code below do the same:

# 1. __new__ and __init__ are called internally
my_instance = SimpleObject(name='bob')

# 2. __new__ and __init__ are called directly:
my_instance = SimpleObject.__new__(SimpleObject)
my_instance.__init__(name='bob')
my_instance.say_hello()

What happens in the direct method:

  1. we call the __new__ function on SimpleObject, passing it the SimpleObject type.
  2. SimpleObject.__new__ calls __new__ on it's parent class (object)
  3. object.__new__ creates and returns a instance of SimpleObject
  4. SimpleObject.__new__ returns the new instance
  5. we call __init__ to initialize it.

These things also happen in the non-direct method but they are handled under the hood so we don't notice.

Simple trick to work with relative paths in Python


Practical application 1: subclassing immutable types

Now that we know how __new__ works we can use if to do some interesting things. We'll put the theory to practice and subclass an immutable type. This way we can have our own, special type with it's own methods defined on a very fast, built-in type.


The goal

We have an application that processes many coordinates. For this reason we want our coordinates stored in tuples since they're small and memory-efficient.

We will create our own Point`` class that inherits fromtuple. This way `Point is a `tuple" so it's very fast and small and we can add functionalities like:

  • control over object creation (only create a new object if all coordinates are positive e.g.)
  • additional methods like calculating the distance between two coordinates.

Cython for absolute beginners: 30x faster code in two simple steps


The Point class with a new override

In our first try we just create a Point class that inherits from tuple and tries to initialize the tuple with a x,y coordinate. This won't work:

class Point(tuple):

  x: float
  y: float

  def __init__(self, x:float, y:float):
    self.x = x
    self.y = y

p = Point(1,2)    # <-- tuple expects 1 argument, got 2

The reason that this fails is because our class is a subclass of the tuple, which are immutable. Remember that the tuple is created by __new__, after which __init__ runs. At the time of initialization, the tuple is already created and cannot be altered anymore since they are immutable.

We can fix this by overriding __new__:

class Point(tuple):

  x: float
  y: float

  def __new__(cls, x:float, y:float):    # <-- newly added method
    return super().__new__(cls, (x, y))

  def __init__(self, x:float, y:float):
    self.x = x
    self.y = y

This works because in __new__ we use super() to get a reference to the parent of Point, which is tuple. Next we use tuple.__new__ and pass it an iterable ((x, y)) create a new tuple. This is the same as tuple((1, 2)).


Controlling instance creation and additional methods

The result is a Point class that is a tuple under the hood but we can add all kinds of extras:

class Point(tuple):
    x: int
    y: int

    def __new__(cls, x:float, y:float):
      if x < 0 or y < 0:                                  # <-- filter inputs
          raise ValueError("x and y must be positive")
      return super().__new__(cls, (x, y))

    def __init__(self, x:float, y:float):
      self.x = x
      self.y = y

    def distance_from(self, other_point: Point):          # <-- new method
      return math.sqrt(
        (other_point.x - self.x) ** 2 + (other_point.y - self.y) ** 2
      )

p = Point(1, 2)
p2 = Point(3, 1)
print(p.distance_from(other_point=p2))  # <-- 2.23606797749979

Notice that we've added a method for calculating distances between Points, as well as some input validation. We now check in __new__ if the provided Xand y values are positive and prevent object creation altogether when this is not the case.

A complete guide to using environment variables and files with Docker and Compose


Practical application 2: adding metadata

In this example we're creating a subclass from an immutable float and add some metadata. The class below will produce a true float but we've added some extra information about the symbol to use.

class Currency(float):

    def __new__(cls, value: float, symbol: str):
        obj = super(Currency, cls).__new__(cls, value)
        obj.symbol = symbol
        return obj

    def __str__(self) -> str:
        return f"{self.symbol} {self:.2f}"  # <-- returns symbol & float formatted to 2 decimals

price = Currency(12.768544, symbol='€')
print(price)                            # <-- prints: "€ 12.74"

As you see we inherit from float , which makes an instance of Currency an actual float. As you see, we also have access to metadata like a symbol for pretty printing.

Also notice that this is an actual float; we can perform float operations without a problem:

print(isinstance(price, float))        # True
print(f"{price.symbol} {price * 2}")   # prints: "€ 25.48"

Args vs kwargs: which is the fastest way to call a function in Python?


Practical application 3: Singleton pattern

There are cases when you don't want to return a new object every time you instantiate a class. A database connection for example. A singleton restricts the instantiation of a class to one single instance. This pattern is used to ensure that a class has only one instance and provides a global point of access to that instance:

class Singleton:
  _instance = None

  def __new__(cls):
    if cls._instance is None:
      cls._instance = super(Singleton, cls).__new__(cls)
    return cls._instance

singleton1 = Singleton()
singleton2 = Singleton()

print(id(singleton1))
print(id(singleton2))
print(singleton1 is singleton2)  # True

This code creates an instance of the Singleton class if it doesn't exist yet and saves it as an attribute on the cls. When Singleton is called once more it returns the instance that is has stored before.

Run Code after Your Program Exits with Python's AtExit


Other Practical applications

Some other applications include:

  • Controlling instance creation We've seen this in the Point example: add additional logic before creating an instance. This can include input validation, modification, or logging.

  • Factory Methods Determine in __new__ which class will be returned, based on inputs.

  • Caching For resource-intensive object creation. Like with the Singleton pattern, we can store previously created objects on the class itself. We can check in __new__if an equivalent object already existst and return it instead of creating a new one.

Create Your Custom, private Python Package That You Can PIP Install From Your Git Repository


Conclusion

In this article we took a deep dive into Python object creation, learnt a lot about how and why it works. Then we looked at some practical examples that demonstrate that we can do a lot of interesting things with our newly acquired knowledge. Controlling object creation can enable you to create efficient classes and professionalize your code significantly.

To improve your code even further, I think the most important part is to truly understand your code, how Python works and apply the right data structures. For this, check out my other articles here or this this presentation.

I hope this article was as clear as I hope it to be but if this is not the case please let me know what I can do to clarify further. In the meantime, check out my other articles on all kinds of programming-related topics like these:

Happy Coding!

— Mike

P.S: like what I'm doing? Follow me!

Mike Huls – Medium

Tags: Coding Data Science Programming Python Software Development

Comment