Python Class Constructors: Control Your Object Instantiation

Python Class Constructors: Control Your Object Instantiation

by Leodanis Pozo Ramos Mar 14, 2022 intermediate python

Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding: Using Python Class Constructors

Class constructors are a fundamental part of object-oriented programming in Python. They allow you to create and properly initialize objects of a given class, making those objects ready to use. Class constructors internally trigger Python’s instantiation process, which runs through two main steps: instance creation and instance initialization.

If you want to dive deeper into how Python internally constructs objects and learn how to customize the process, then this tutorial is for you.

In this tutorial, you’ll:

  • Understand Python’s internal instantiation process
  • Customize object initialization using .__init__()
  • Fine-tune object creation by overriding .__new__()

With this knowledge, you’ll be able to tweak the creation and initialization of objects in your custom Python classes, which will give you control over the instantiation process at a more advanced level.

To better understand the examples and concepts in this tutorial, you should be familiar with object-oriented programming and special methods in Python.

Python’s Class Constructors and the Instantiation Process

Like many other programming languages, Python supports object-oriented programming. At the heart of Python’s object-oriented capabilities, you’ll find the class keyword, which allows you to define custom classes that can have attributes for storing data and methods for providing behaviors.

Once you have a class to work with, then you can start creating new instances or objects of that class, which is an efficient way to reuse functionality in your code.

Creating and initializing objects of a given class is a fundamental step in object-oriented programming. This step is often referred to as object construction or instantiation. The tool responsible for running this instantiation process is commonly known as a class constructor.

Getting to Know Python’s Class Constructors

In Python, to construct an object of a given class, you just need to call the class with appropriate arguments, as you would call any function:

>>>
>>> class SomeClass:
...     pass
...

>>> # Call the class to construct an object
>>> SomeClass()
<__main__.SomeClass object at 0x7fecf442a140>

In this example, you define SomeClass using the class keyword. This class is currently empty because it doesn’t have attributes or methods. Instead, the class’s body only contains a pass statement as a placeholder statement that does nothing.

Then you create a new instance of SomeClass by calling the class with a pair of parentheses. In this example, you don’t need to pass any argument in the call because your class doesn’t take arguments yet.

In Python, when you call a class as you did in the above example, you’re calling the class constructor, which creates, initializes, and returns a new object by triggering Python’s internal instantiation process.

A final point to note is that calling a class isn’t the same as calling an instance of a class. These are two different and unrelated topics. To make a class’s instance callable, you need to implement a .__call__() special method, which has nothing to do with Python’s instantiation process.

Understanding Python’s Instantiation Process

You trigger Python’s instantiation process whenever you call a Python class to create a new instance. This process runs through two separate steps, which you can describe as follows:

  1. Create a new instance of the target class
  2. Initialize the new instance with an appropriate initial state

To run the first step, Python classes have a special method called .__new__(), which is responsible for creating and returning a new empty object. Then another special method, .__init__(), takes the resulting object, along with the class constructor’s arguments.

The .__init__() method takes the new object as its first argument, self. Then it sets any required instance attribute to a valid state using the arguments that the class constructor passed to it.

In short, Python’s instantiation process starts with a call to the class constructor, which triggers the instance creator, .__new__(), to create a new empty object. The process continues with the instance initializer, .__init__(), which takes the constructor’s arguments to initialize the newly created object.

To explore how Python’s instantiation process works internally, consider the following example of a Point class that implements a custom version of both methods, .__new__() and .__init__(), for demonstration purposes:

 1# point.py
 2
 3class Point:
 4    def __new__(cls, *args, **kwargs):
 5        print("1. Create a new instance of Point.")
 6        return super().__new__(cls)
 7
 8    def __init__(self, x, y):
 9        print("2. Initialize the new instance of Point.")
10        self.x = x
11        self.y = y
12
13    def __repr__(self) -> str:
14        return f"{type(self).__name__}(x={self.x}, y={self.y})"

Here’s a breakdown of what this code does:

  • Line 3 defines the Point class using the class keyword followed by the class name.

  • Line 4 defines the .__new__() method, which takes the class as its first argument. Note that using cls as the name of this argument is a strong convention in Python, just like using self to name the current instance is. The method also takes *args and **kwargs, which allow for passing an undefined number of initialization arguments to the underlying instance.

  • Line 5 prints a message when .__new__() runs the object creation step.

  • Line 6 creates a new Point instance by calling the parent class’s .__new__() method with cls as an argument. In this example, object is the parent class, and the call to super() gives you access to it. Then the instance is returned. This instance will be the first argument to .__init__().

  • Line 8 defines .__init__(), which is responsible for the initialization step. This method takes a first argument called self, which holds a reference to the current instance. The method also takes two additional arguments, x and y. These arguments hold initial values for the instance attributes .x and .y. You need to pass suitable values for these arguments in the call to Point(), as you’ll learn in a moment.

  • Line 9 prints a message when .__init__() runs the object initialization step.

  • Lines 10 and 11 initialize .x and .y, respectively. To do this, they use the provided input arguments x and y.

  • Lines 13 and 14 implement the .__repr__() special method, which provides a proper string representation for your Point class.

With Point in place, you can uncover how the instantiation process works in practice. Save your code to a file called point.py and start your Python interpreter in a command-line window. Then run the following code:

>>>
>>> from point import Point

>>> point = Point(21, 42)
1. Create a new instance of Point.
2. Initialize the new instance of Point.

>>> point
Point(x=21, y=42)

Calling the Point() class constructor creates, initializes, and returns a new instance of the class. This instance is then assigned to the point variable.

In this example, the call to the constructor also lets you know the steps that Python internally runs to construct the instance. First, Python calls .__new__() and then .__init__(), resulting in a new and fully initialized instance of Point, as you confirmed at the end of the example.

To continue learning about class instantiation in Python, you can try running both steps manually:

>>>
>>> from point import Point

>>> point = Point.__new__(Point)
1. Create a new instance of Point.

>>> # The point object is not initialized
>>> point.x
Traceback (most recent call last):
    ...
AttributeError: 'Point' object has no attribute 'x'
>>> point.y
Traceback (most recent call last):
    ...
AttributeError: 'Point' object has no attribute 'y'

>>> point.__init__(21, 42)
2. Initialize the new instance of Point.

>>> # Now point is properly initialized
>>> point
Point(x=21, y=42)

In this example, you first call .__new__() on your Point class, passing the class itself as the first argument to the method. This call only runs the first step of the instantiation process, creating a new and empty object. Note that creating an instance this way bypasses the call to .__init__().

Once you have the new object, then you can initialize it by calling .__init__() with an appropriate set of arguments. After this call, your Point object is properly initialized, with all its attributes set up.

A subtle and important detail to note about .__new__() is that it can also return an instance of a class different from the class that implements the method itself. When that happens, Python doesn’t call .__init__() in the current class, because there’s no way to unambiguously know how to initialize an object of a different class.

Consider the following example, in which the .__new__() method of the B class returns an instance of the A class:

# ab_classes.py

class A:
    def __init__(self, a_value):
        print("Initialize the new instance of A.")
        self.a_value = a_value

class B:
    def __new__(cls, *args, **kwargs):
        return A(42)

    def __init__(self, b_value):
        print("Initialize the new instance of B.")
        self.b_value = b_value

Because B.__new__() returns an instance of a different class, Python doesn’t run B.__init__(). To confirm this behavior, save the code into a file called ab_classes.py and then run the following code in an interactive Python session:

>>>
>>> from ab_classes import B

>>> b = B(21)
Initialize the new instance of A.

>>> b.b_value
Traceback (most recent call last):
    ...
AttributeError: 'A' object has no attribute 'b_value'

>>> isinstance(b, B)
False
>>> isinstance(b, A)
True

>>> b.a_value
42

The call to the B() class constructor runs B.__new__(), which returns an instance of A instead of B. That’s why B.__init__() never runs. Note that b doesn’t have a .b_value attribute. In contrast, b does have an .a_value attribute with a value of 42.

Now that you know the steps that Python internally takes to create instances of a given class, you’re ready to dig a little deeper into other characteristics of .__init__(), .__new__(), and the steps that they run.

Object Initialization With .__init__()

In Python, the .__init__() method is probably the most common special method that you’ll ever override in your custom classes. Almost all your classes will need a custom implementation of .__init__(). Overriding this method will allow you to initialize your objects properly.

The purpose of this initialization step is to leave your new objects in a valid state so that you can start using them right away in your code. In this section, you’ll learn the basics of writing your own .__init__() methods and how they can help you customize your classes.

Providing Custom Object Initializers

The most bare-bones implementation of .__init__() that you can write will just take care of assigning input arguments to matching instance attributes. For example, say you’re writing a Rectangle class that requires .width and .height attributes. In that case, you can do something like this:

>>>
>>> class Rectangle:
...     def __init__(self, width, height):
...         self.width = width
...         self.height = height
...

>>> rectangle = Rectangle(21, 42)
>>> rectangle.width
21
>>> rectangle.height
42

As you learned before, .__init__() runs the second step of the object instantiation process in Python. Its first argument, self, holds the new instance that results from calling .__new__(). The rest of the arguments to .__init__() are normally used to initialize instance attributes. In the above example, you initialized the rectangle’s .width and .height using the width and height arguments to .__init__().

It’s important to note that, without counting self, the arguments to .__init__() are the same ones that you passed in the call to the class constructor. So, in a way, the .__init__() signature defines the signature of the class constructor.

Additionally, keep in mind that .__init__() must not explicitly return anything different from None, or you’ll get a TypeError exception:

>>>
>>> class Rectangle:
...     def __init__(self, width, height):
...         self.width = width
...         self.height = height
...         return 42
...

>>> rectangle = Rectangle(21, 42)
Traceback (most recent call last):
    ...
TypeError: __init__() should return None, not 'int'

In this example, the .__init__() method attempts to return an integer number, which ends up raising a TypeError exception at run time.

The error message in the above example says that .__init__() should return None. However, you don’t need to return None explicitly, because methods and functions without an explicit return statement just return None implicitly in Python.

With the above implementation of .__init__(), you ensure that .width and .height get initialized to a valid state when you call the class constructor with appropriate arguments. That way, your rectangles will be ready for use right after the construction process finishes.

In .__init__(), you can also run any transformation over the input arguments to properly initialize the instance attributes. For example, if your users will use Rectangle directly, then you might want to validate the supplied width and height and make sure that they’re correct before initializing the corresponding attributes:

>>>
>>> class Rectangle:
...     def __init__(self, width, height):
...         if not (isinstance(width, (int, float)) and width > 0):
...             raise ValueError(f"positive width expected, got {width}")
...         self.width = width
...         if not (isinstance(height, (int, float)) and height > 0):
...             raise ValueError(f"positive height expected, got {height}")
...         self.height = height
...

>>> rectangle = Rectangle(-21, 42)
Traceback (most recent call last):
    ...
ValueError: positive width expected, got -21

In this updated implementation of .__init__(), you make sure that the input width and height arguments are positive numbers before initializing the corresponding .width and .height attributes. If either validation fails, then you get a ValueError.

Now say that you’re using inheritance to create a custom class hierarchy and reuse some functionality in your code. If your subclasses provide a .__init__() method, then this method must explicitly call the base class’s .__init__() method with appropriate arguments to ensure the correct initialization of instances. To do this, you should use the built-in super() function like in the following example:

>>>
>>> class Person:
...     def __init__(self, name, birth_date):
...         self.name = name
...         self.birth_date = birth_date
...

>>> class Employee(Person):
...     def __init__(self, name, birth_date, position):
...         super().__init__(name, birth_date)
...         self.position = position
...

>>> john = Employee("John Doe", "2001-02-07", "Python Developer")

>>> john.name
'John Doe'
>>> john.birth_date
'2001-02-07'
>>> john.position
'Python Developer'

The first line in the .__init__() method of Employee calls super().__init__() with name and birth_date as arguments. This call ensures the initialization of .name and .birth_date in the parent class, Person. This technique allows you to extend the base class with new attributes and functionality.

To wrap up this section, you should know that the base implementation of .__init__() comes from the built-in object class. This implementation is automatically called when you don’t provide an explicit .__init__() method in your classes.

Building Flexible Object Initializers

You can make your objects’ initialization step flexible and versatile by tweaking the .__init__() method. To this end, one of the most popular techniques is to use optional arguments. This technique allows you to write classes in which the constructor accepts different sets of input arguments at instantiation time. Which arguments to use at a given time will depend on your specific needs and context.

As a quick example, check out the following Greeter class:

# greet.py

class Greeter:
    def __init__(self, name, formal=False):
        self.name = name
        self.formal = formal

    def greet(self):
        if self.formal:
            print(f"Good morning, {self.name}!")
        else:
            print(f"Hello, {self.name}!")

In this example, .__init__() takes a regular argument called name. It also takes an optional argument called formal, which defaults to False. Because formal has a default value, you can construct objects by relying on this value or by providing your own.

The class’s final behavior will depend on the value of formal. If this argument is False, then you’ll get an informal greeting when you call .greet(). Otherwise, you’ll get a more formal greeting.

To try Greeter out, go ahead and save the code into a greet.py file. Then open an interactive session in your working directory and run the following code:

>>>
>>> from greet import Greeter

>>> informal_greeter = Greeter("Pythonista")
>>> informal_greeter.greet()
Hello, Pythonista!

>>> formal_greeter = Greeter("Pythonista", formal=True)
>>> formal_greeter.greet()
Good morning, Pythonista!

In the first example, you create an informal_greeter object by passing a value to the name argument and relying on the default value of formal. You get an informal greeting on your screen when you call .greet() on the informal_greeter object.

In the second example, you use a name and a formal argument to instantiate Greeter. Because formal is True, the result of calling .greet() is a formal greeting.

Even though this is a toy example, it showcases how default argument values are a powerful Python feature that you can use to write flexible initializers for your classes. These initializers will allow you to instantiate your classes using different sets of arguments depending on your needs.

Okay! Now that you know the basics of .__init__() and the object initialization step, it’s time to change gears and start diving deeper into .__new__() and the object creation step.

Object Creation With .__new__()

When writing Python classes, you typically don’t need to provide your own implementation of the .__new__() special method. Most of the time, the base implementation from the built-in object class is sufficient to build an empty object of your current class.

However, there are a few interesting use cases for this method. For example, you can use .__new__() to create subclasses of immutable types, such as int, float, tuple, and str.

In the following sections, you’ll learn how to write custom implementations of .__new__() in your classes. To do this, you’ll code a few examples that’ll give you an idea of when you might need to override this method.

Providing Custom Object Creators

Typically, you’ll write a custom implementation of .__new__() only when you need to control the creation of a new instance at a low level. Now, if you need a custom implementation of this method, then you should follow a few steps:

  1. Create a new instance by calling super().__new__() with appropriate arguments.
  2. Customize the new instance according to your specific needs.
  3. Return the new instance to continue the instantiation process.

With these three succinct steps, you’ll be able to customize the instance creation step in the Python instantiation process. Here’s an example of how you can translate these steps into Python code:

class SomeClass:
    def __new__(cls, *args, **kwargs):
        instance = super().__new__(cls)
        # Customize your instance here...
        return instance

This example provides a sort of template implementation of .__new__(). As usual, .__new__() takes the current class as an argument that’s typically called cls.

Note that you’re using *args and **kwargs to make the method more flexible and maintainable by accepting any number of arguments. You should always define .__new__() with *args and **kwargs, unless you have a good reason to follow a different pattern.

In the first line of .__new__(), you call the parent class’s .__new__() method to create a new instance and allocate memory for it. To access the parent class’s .__new__() method, you use the super() function. This chain of calls takes you up to object.__new__(), which is the base implementation of .__new__() for all Python classes.

The next step is to customize your newly created instance. You can do whatever you need to do to customize the instance at hand. Finally, in the third step, you need to return the new instance to continue the instantiation process with the initialization step.

It’s important to note that object.__new__() itself only accepts a single argument, the class to instantiate. If you call object.__new__() with more arguments, then you get a TypeError:

>>>
>>> class SomeClass:
...     def __new__(cls, *args, **kwargs):
...         return super().__new__(cls, *args, **kwargs)
...     def __init__(self, value):
...         self.value = value
...

>>> SomeClass(42)
Traceback (most recent call last):
    ...
TypeError: object.__new__() takes exactly one argument (the type to instantiate)

In this example, you hand over *args and **kwargs as additional arguments in the call to super().__new__(). The underlying object.__new__() accepts only the class as an argument, so you get a TypeError when you instantiate the class.

However, object.__new__() still accepts and passes over extra arguments to .__init__() if your class doesn’t override .__new__(), as in the following variation of SomeClass:

>>>
>>> class SomeClass:
...     def __init__(self, value):
...         self.value = value
...

>>> some_obj = SomeClass(42)
>>> some_obj
<__main__.SomeClass object at 0x7f67db8d0ac0>
>>> some_obj.value
42

In this implementation of SomeClass, you don’t override .__new__(). The object creation is then delegated to object.__new__(), which now accepts value and passes it over to SomeClass.__init__() to finalize the instantiation. Now you can create new and fully initialized instances of SomeClass, just like some_obj in the example.

Cool! Now that you know the basics of writing your own implementations of .__new__(), you’re ready to dive into a few practical examples that feature some of the most common use cases of this method in Python programming.

Subclassing Immutable Built-in Types

To kick things off, you’ll start with a use case of .__new__() that consists of subclassing an immutable built-in type. As an example, say you need to write a Distance class as a subclass of Python’s float type. Your class will have an additional attribute to store the unit that’s used to measure the distance.

Here’s a first approach to this problem, using the .__init__() method:

>>>
>>> class Distance(float):
...     def __init__(self, value, unit):
...         super().__init__(value)
...         self.unit = unit
...

>>> in_miles = Distance(42.0, "Miles")
Traceback (most recent call last):
    ...
TypeError: float expected at most 1 argument, got 2

When you subclass an immutable built-in data type, you get an error. Part of the problem is that the value is set during creation, and it’s too late to change it during initialization. Additionally, float.__new__() is called under the hood, and it doesn’t deal with extra arguments in the same way as object.__new__(). This is what raises the error in your example.

To work around this issue, you can initialize the object at creation time with .__new__() instead of overriding .__init__(). Here’s how you can do this in practice:

>>>
>>> class Distance(float):
...     def __new__(cls, value, unit):
...         instance = super().__new__(cls, value)
...         instance.unit = unit
...         return instance
...

>>> in_miles = Distance(42.0, "Miles")
>>> in_miles
42.0
>>> in_miles.unit
'Miles'
>>> in_miles + 42.0
84.0

>>> dir(in_miles)
['__abs__', '__add__', ..., 'real', 'unit']

In this example, .__new__() runs the three steps that you learned in the previous section. First, the method creates a new instance of the current class, cls, by calling super().__new__(). This time, the call rolls back to float.__new__(), which creates a new instance and initializes it using value as an argument. Then the method customizes the new instance by adding a .unit attribute to it. Finally, the new instance gets returned.

That’s it! Now your Distance class works as expected, allowing you to use an instance attribute for storing the unit in which you’re measuring the distance. Unlike the floating-point value stored in a given instance of Distance, the .unit attribute is mutable, so you can change its value any time you like. Finally, note how a call to the dir() function reveals that your class inherits features and methods from float.

Returning Instances of a Different Class

Returning an object of a different class is a requirement that can raise the need for a custom implementation of .__new__(). However, you should be careful because in this case, Python skips the initialization step entirely. So, you’ll have the responsibility of taking the newly created object into a valid state before using it in your code.

Check out the following example, in which the Pet class uses .__new__() to return instances of randomly selected classes:

# pets.py

from random import choice

class Pet:
    def __new__(cls):
        other = choice([Dog, Cat, Python])
        instance = super().__new__(other)
        print(f"I'm a {type(instance).__name__}!")
        return instance

    def __init__(self):
        print("Never runs!")

class Dog:
    def communicate(self):
        print("woof! woof!")

class Cat:
    def communicate(self):
        print("meow! meow!")

class Python:
    def communicate(self):
        print("hiss! hiss!")

In this example, Pet provides a .__new__() method that creates a new instance by randomly selecting a class from a list of existing classes.

Here’s how you can use this Pet class as a factory of pet objects:

>>>
>>> from pets import Pet

>>> pet = Pet()
I'm a Dog!
>>> pet.communicate()
woof! woof!
>>> isinstance(pet, Pet)
False
>>> isinstance(pet, Dog)
True

>>> another_pet = Pet()
I'm a Python!
>>> another_pet.communicate()
hiss! hiss!

Every time you instantiate Pet, you get a random object from a different class. This result is possible because there’s no restriction on the object that .__new__() can return. Using .__new__() in such a way transforms a class into a flexible and powerful factory of objects, not limited to instances of itself.

Finally, note how the .__init__() method of Pet never runs. That’s because Pet.__new__() always returns objects of a different class rather than of Pet itself.

Allowing Only a Single Instance in Your Classes

Sometimes you need to implement a class that allows the creation of a single instance only. This type of class is commonly known as a singleton class. In this situation, the .__new__() method comes in handy because it can help you restrict the number of instances that a given class can have.

Here’s an example of coding a Singleton class with a .__new__() method that allows the creation of only one instance at a time. To do this, .__new__() checks the existence of previous instances cached on a class attribute:

>>>
>>> class Singleton(object):
...     _instance = None
...     def __new__(cls, *args, **kwargs):
...         if cls._instance is None:
...             cls._instance = super().__new__(cls)
...         return cls._instance
...

>>> first = Singleton()
>>> second = Singleton()
>>> first is second
True

The Singleton class in this example has a class attribute called ._instance that defaults to None and works as a cache. The .__new__() method checks if no previous instance exists by testing the condition cls._instance is None.

If this condition is true, then the if code block creates a new instance of Singleton and stores it to cls._instance. Finally, the method returns the new or the existing instance to the caller.

Then you instantiate Singleton twice to try to construct two different objects, first and second. If you compare the identity of these objects with the is operator, then you’ll note that both objects are the same object. The names first and second just hold references to the same Singleton object.

Partially Emulating collections.namedtuple

As a final example of how to take advantage of .__new__() in your code, you can push your Python skills and write a factory function that partially emulates collections.namedtuple(). The namedtuple() function allows you to create subclasses of tuple with the additional feature of having named fields for accessing the items in the tuple.

The code below implements a named_tuple_factory() function that partially emulates this functionality by overriding the .__new__() method of a nested class called NamedTuple:

 1# named_tuple.py
 2
 3from operator import itemgetter
 4
 5def named_tuple_factory(type_name, *fields):
 6    num_fields = len(fields)
 7
 8    class NamedTuple(tuple):
 9        __slots__ = ()
10
11        def __new__(cls, *args):
12            if len(args) != num_fields:
13                raise TypeError(
14                    f"{type_name} expected exactly {num_fields} arguments,"
15                    f" got {len(args)}"
16                )
17            cls.__name__ = type_name
18            for index, field in enumerate(fields):
19                setattr(cls, field, property(itemgetter(index)))
20            return super().__new__(cls, args)
21
22        def __repr__(self):
23            return f"""{type_name}({", ".join(repr(arg) for arg in self)})"""
24
25    return NamedTuple

Here’s how this factory function works line by line:

  • Line 3 imports itemgetter() from the operators module. This function allows you to retrieve items using their index in the containing sequence.

  • Line 5 defines named_tuple_factory(). This function takes a first argument called type_name, which will hold the name of the tuple subclass that you want to create. The *fields argument allows you to pass an undefined number of field names as strings.

  • Line 6 defines a local variable to hold the number of named fields provided by the user.

  • Line 8 defines a nested class called NamedTuple, which inherits from the built-in tuple class.

  • Line 9 provides a .__slots__ class attribute. This attribute defines a tuple for holding instance attributes. This tuple saves memory by acting as a substitute for the instance’s dictionary, .__dict__, which would otherwise play a similar role.

  • Line 11 implements .__new__() with cls as its first argument. This implementation also takes the *args argument to accept an undefined number of field values.

  • Lines 12 to 16 define a conditional statement that checks if the number of items to store in the final tuple differs from the number of named fields. If that’s the case, then the conditional raises a TypeError with an error message.

  • Line 17 sets the .__name__ attribute of the current class to the value provided by type_name.

  • Lines 18 and 19 define a for loop that turns every named field into a property that uses itemgetter() to return the item at the target index. The loop uses the built-in setattr() function to perform this action. Note that the built-in enumerate() function provides the appropriate index value.

  • Line 20 returns a new instance of the current class by calling super().__new__() as usual.

  • Lines 22 and 23 define a .__repr__() method for your tuple subclass.

  • Line 25 returns the newly created NamedTuple class.

To try your named_tuple_factory() out, fire up an interactive session in the directory containing the named_tuple.py file and run the following code:

>>>
>>> from named_tuple import named_tuple_factory

>>> Point = named_tuple_factory("Point", "x", "y")

>>> point = Point(21, 42)
>>> point
Point(21, 42)
>>> point.x
21
>>> point.y
42
>>> point[0]
21
>>> point[1]
42

>>> point.x = 84
Traceback (most recent call last):
    ...
AttributeError: can't set attribute

>>> dir(point)
['__add__', '__class__', ..., 'count', 'index', 'x', 'y']

In this code snippet, you create a new Point class by calling named_tuple_factory(). The first argument in this call represents the name that the resulting class object will use. The second and third arguments are the named fields available in the resulting class.

Then you create a Point object by calling the class constructor with appropriate values for the .x and .y fields. To access the value of each named field, you can use the dot notation. You can also use indices to retrieve the values because your class is a tuple subclass.

Because tuples are immutable data types in Python, you can’t assign new values to the point’s coordinates in place. If you try to do that, then you get an AttributeError.

Finally, calling dir() with your point instance as an argument reveals that your object inherits all the attributes and methods that regular tuples have in Python.

Conclusion

Now you know how Python class constructors allow you to instantiate classes, so you can create concrete and ready-to-use objects in your code. In Python, class constructors internally trigger the instantiation or construction process, which goes through instance creation and instance initialization. These steps are driven by the .__new__() and .__init__() special methods.

By learning about Python’s class constructors, the instantiation process, and the .__new__() and .__init__() methods, you can now manage how your custom classes construct new instances.

In this tutorial, you learned:

  • How Python’s instantiation process works internally
  • How your own .__init__() methods help you customize object initialization
  • How overriding the .__new__() method allows for custom object creation

Now you’re ready to take advantage of this knowledge to fine-tune your class constructors and take full control over instance creation and initialization in your object-oriented programming adventure with Python.

Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding: Using Python Class Constructors

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Leodanis Pozo Ramos

Leodanis Pozo Ramos Leodanis Pozo Ramos

Leodanis is an industrial engineer who loves Python and software development. He's a self-taught Python developer with 6+ years of experience. He's an avid technical writer with a growing number of articles published on Real Python and other sites.

» More about Leodanis

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:
Tweet Share Share Email

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!

Keep Learning

Related Tutorial Categories: intermediate python

Recommended Video Course: Using Python Class Constructors