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.
Free Bonus: Click here to get access to a free Python OOP Cheat Sheet that points you to the best tutorials, videos, and books to learn more about Object-Oriented Programming with Python.
Take the Quiz: Test your knowledge with our interactive “Python Class Constructors: Control Your Object Instantiation” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Python Class Constructors: Control Your Object InstantiationIn this quiz, you'll test your understanding of class constructors in Python. By working through this quiz, you'll revisit the internal instantiation process, object initialization, and fine-tuning object creation.
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:
- Create a new instance of the target class
- 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 theclass
keyword followed by the class name. -
Line 4 defines the
.__new__()
method, which takes the class as its first argument. Note that usingcls
as the name of this argument is a strong convention in Python, just like usingself
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 withcls
as an argument. In this example,object
is the parent class, and the call tosuper()
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 calledself
, which holds a reference to the current instance. The method also takes two additional arguments,x
andy
. These arguments hold initial values for the instance attributes.x
and.y
. You need to pass suitable values for these arguments in the call toPoint()
, 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 argumentsx
andy
. -
Lines 13 and 14 implement the
.__repr__()
special method, which provides a proper string representation for yourPoint
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__()
.
Note: The code snippet above is intended to be a demonstrative example of how the instantiation process works internally. It’s not something that you would typically do in real code.
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
.
Note: A more Pythonic technique to tackle attribute validation is to turn attributes into properties. To learn more about properties, check out Python’s property(): Add Managed Attributes to Your Classes.
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:
- Create a new instance by calling
super().__new__()
with appropriate arguments. - Customize the new instance according to your specific needs.
- 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.
Note: The built-in object
class is the default base class of 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.
Note: The Distance
class in the example above doesn’t provide a proper unit conversion mechanism. This means that something like Distance(10, "km") + Distance(20, "miles")
won’t attempt at converting units before adding the values. If you’re interested in converting units, then check out the Pint project on PyPI.
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.
Note: Most experienced Python developers would argue that you don’t need to implement the singleton design pattern in Python unless you already have a working class and need to add the pattern’s functionality on top of it.
The rest of the time, you can use a module-level constant to get the same singleton functionality without having to write a relatively complex class.
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
.
Note: In the example above, Singleton
doesn’t provide an implementation of .__init__()
. If you ever need a class like this with a .__init__()
method, then keep in mind that this method will run every time you call the Singleton()
constructor. This behavior can cause weird initialization effects and bugs.
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 theoperator
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 calledtype_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-intuple
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__()
withcls
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 bytype_name
. -
Lines 18 and 19 define a
for
loop that turns every named field into a property that usesitemgetter()
to return the item at the targetindex
. The loop uses the built-insetattr()
function to perform this action. Note that the built-inenumerate()
function provides the appropriateindex
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.
Free Bonus: Click here to get access to a free Python OOP Cheat Sheet that points you to the best tutorials, videos, and books to learn more about Object-Oriented Programming with Python.
Take the Quiz: Test your knowledge with our interactive “Python Class Constructors: Control Your Object Instantiation” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Python Class Constructors: Control Your Object InstantiationIn this quiz, you'll test your understanding of class constructors in Python. By working through this quiz, you'll revisit the internal instantiation process, object initialization, and fine-tuning object creation.
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