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 Multiple Constructors in Your Python Classes
Sometimes you need to write a Python class that provides multiple ways to construct objects. In other words, you want a class that implements multiple constructors. This kind of class comes in handy when you need to create instances using different types or numbers of arguments. Having the tools to provide multiple constructors will help you write flexible classes that can adapt to changing needs.
In Python, there are several techniques and tools that you can use to construct classes, including simulating multiple constructors through optional arguments, customizing instance creation via class methods, and doing special dispatch with decorators. If you want to learn about these techniques and tools, then this tutorial is for you.
In this tutorial, you’ll learn how to:
- Use optional arguments and type checking to simulate multiple constructors
- Write multiple constructors using the built-in
@classmethod
decorator - Overload your class constructors using the
@singledispatchmethod
decorator
You’ll also get a peek under the hood at how Python internally constructs instances of a regular class and how some standard-library classes provide multiple constructors.
To get the most out of this tutorial, you should have basic knowledge of object-oriented programming and understand how to define class methods with @classmethod
. You should also have experience working with decorators 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.
Instantiating Classes in Python
Python supports object-oriented programming with classes that are straightforward to create and use. Python classes offer powerful features that can help you write better software. Classes are like blueprints for objects, also known as instances. In the same way that you can build several houses from a single blueprint, you can build several instances from a class.
To define a class in Python, you need to use the class
keyword followed by the class name:
>>> # Define a Person class
>>> class Person:
... def __init__(self, name):
... self.name = name
...
Python has a rich set of special methods that you can use in your classes. Python implicitly calls special methods to automatically execute a wide variety of operations on instances. There are special methods to make your objects iterable, provide a suitable string representation for your objects, initialize instance attributes, and a lot more.
A pretty common special method is .__init__()
. This method provides what’s known as the instance initializer in Python. This method’s job is to initialize instance attributes with appropriate values when you instantiate a given class.
In Person
, the .__init__()
method’s first argument is called self
. This argument holds the current object or instance, which is passed implicitly in the method call. This argument is common to every instance method in Python. The second argument to .__init__()
is called name
and will hold the person’s name as a string.
Note: Using self
to name the current object is a pretty strong convention in Python but not a requirement. However, using another name will raise some eyebrows among your fellow Python developers.
Once you’ve defined a class, you can start instantiating it. In other words, you can start creating objects of that class. To do this, you’ll use a familiar syntax. Just call the class using a pair of parentheses (()
), which is the same syntax that you use to call any Python function:
>>> # Instantiating Person
>>> john = Person("John Doe")
>>> john.name
'John Doe'
In Python, the class name provides what other languages, such as C++ and Java, call the class constructor. Calling a class, like you did with Person
, triggers Python’s class instantiation process, which internally runs in two steps:
- Create a new instance of the target class.
- Initialize the instance with suitable instance attribute values.
To continue with the above example, the value that you pass as an argument to Person
is internally passed to .__init__()
and then assigned to the instance attribute .name
. This way, you initialize your person instance, john
, with valid data, which you can confirm by accessing .name
. Success! John Doe
is indeed his name.
Note: When you call the class to create a new instance, you need to provide as many arguments as .__init__()
requires so that this method can initialize all the instance attributes that demand an initial value.
Now that you understand the object initialization mechanism, you’re ready to learn what Python does before it gets to this point in the instantiation process. It’s time to dig into another special method, called .__new__()
. This method takes care of creating new instances in Python.
Note: The .__new__()
special method is often called a class constructor in Python. However, its job is actually to create new objects from the class blueprint, so you can more accurately call it an instance creator or object creator.
The .__new__()
special method takes the underlying class as its first argument and returns a new object. This object is typically an instance of the input class, but in some cases, it can be an instance of a different class.
If the object that .__new__()
returns is an instance of the current class, then this instance is immediately passed to .__init__()
for initialization purposes. These two steps run when you call the class.
Python’s object
class provides the base or default implementation of .__new__()
and .__init__()
. Unlike with .__init__()
, you rarely need to override .__new__()
in your custom classes. Most of the time, you can safely rely on its default implementation.
To summarize what you’ve learned so far, Python’s instantiation process starts when you call a class with appropriate arguments. Then the process runs through two steps: object creation with the .__new__()
method, and object initialization with the .__init__()
method.
Now that you know about this internal behavior of Python, you’re ready to dive into providing multiple constructors in your classes. In other words, you’ll provide multiple ways to construct objects of a given Python class.
Defining Multiple Class Constructors
Sometimes you’d like to write a class that allows you to construct objects using arguments of different data types or even a different number of arguments. One way to achieve this is by providing multiple constructors in the class at hand. Each constructor will allow you to create instances of the class using a different set of arguments.
Some programming languages, such as C++, C#, and Java, support what is known as function or method overloading. This feature allows you to provide multiple class constructors because it enables you to create multiple functions or methods with the same name and different implementations.
Method overloading means that depending on how you call the method at hand, the language will select the appropriate implementation to run. So, your method can perform different tasks according to the context of the call.
Unfortunately, Python doesn’t support function overloading directly. Python classes keep method names in an internal dictionary called .__dict__
, which holds the class namespace. Like any Python dictionary, .__dict__
can’t have repeated keys, so you can’t have multiple methods with the same name in a given class. If you try to do so, then Python will only remember the last implementation of the method at hand:
# greet.py
class Greeter:
def say_hello(self):
print("Hello, World")
def say_hello(self):
print("Hello, Pythonista")
In this example, you create Greeter
as a Python class with two methods. Both methods have the same name, but they have slightly different implementations.
To learn what happens when two methods have the same name, save your class into a greet.py
file in your working directory and run the following code in an interactive session:
>>> from greet import Greeter
>>> greeter = Greeter()
>>> greeter.say_hello()
Hello, Pythonista
>>> Greeter.__dict__
mappingproxy({..., 'say_hello': <function Greeter.say_hello at...>, ...})
In this example, you call .say_hello()
on greeter
, which is an instance of the Greeter
class. You get Hello, Pythonista
instead of Hello, World
on your screen, which confirms that the second implementation of the method prevails over the first one.
The final line of code inspects the content of .__dict__
, uncovering that the method’s name, say_hello
, appears only once in the class namespace. This is consistent with how dictionaries work in Python.
Something similar occurs with functions in a Python module and in an interactive session. The last implementation of several functions with the same name prevails over the rest of the implementations:
>>> def say_hello():
... print("Hello, World")
...
>>> def say_hello():
... print("Hello, Pythonista")
...
>>> say_hello()
Hello Pythonista
You define two functions with the same name, say_hello()
, in the same interpreter session. However, the second definition overwrites the first one. When you call the function, you get Hello, Pythonista
, which confirms that the last function definition prevails.
Another technique that some programming languages use to provide multiple ways to call a method or function is multiple dispatch.
With this technique, you can write several different implementations of the same method or function and dynamically dispatch the desired implementation according to the type or other characteristics of the arguments that are used in the call. You can use a couple of tools from the standard library to pull this technique into your Python code.
Python is a fairly flexible and feature-rich language and provides a couple of ways to implement multiple constructors and make your classes more flexible.
In the following section, you’ll simulate multiple constructors by passing optional arguments and by checking the argument types to determine different behaviors in your instance initializers.
Simulating Multiple Constructors in Your Classes
A pretty useful technique for simulating multiple constructors in a Python class is to provide .__init__()
with optional arguments using default argument values. This way, you can call the class constructor in different ways and get a different behavior each time.
Another strategy is to check the data type of the arguments to .__init__()
to provide different behaviors depending on the concrete data type that you pass in the call. This technique allows you to simulate multiple constructors in a class.
In this section, you’ll learn the basics of how to simulate multiple ways to construct objects by providing appropriate default values for the arguments of the .__init__()
method and also by checking the data type of the arguments to this method. Both approaches require only one implementation of .__init__()
.
Using Optional Argument Values in .__init__()
An elegant and Pythonic way to simulate multiple constructors is to implement a .__init__()
method with optional arguments. You can do this by specifying appropriate default argument values.
Note: You can also provide optional arguments in your functions and methods using an undefined number of positional arguments or an undefined number of keyword arguments. Check out Using Python Optional Arguments When Defining Functions for more details on these options.
To this end, say you need to code a factory class called CumulativePowerFactory
. This class will create callable objects that compute specific powers, using a stream of numbers as input. You also need your class to track the total sum of consecutive powers. Finally, your class should accept an argument holding an initial value for the sum of powers.
Go ahead and create a power.py
file in your current directory. Then type in the following code to implement CumulativePowerFactory
:
# power.py
class CumulativePowerFactory:
def __init__(self, exponent=2, *, start=0):
self._exponent = exponent
self.total = start
def __call__(self, base):
power = base ** self._exponent
self.total += power
return power
The initializer of CumulativePowerFactory
takes two optional arguments, exponent
and start
. The first argument holds the exponent that you’ll use to compute a series of powers. It defaults to 2
, which is a commonly used value when it comes to computing powers.
The star or asterisk symbol (*
) after exponent
means that start
is a keyword-only argument. To pass a value to a keyword-only argument, you need to use the argument’s name explicitly. In other words, to set arg
to value
, you need to explicitly type arg=value
.
The start
argument holds the initial value to compute the cumulative sum of powers. It defaults to 0
, which is the appropriate value for those cases in which you don’t have a previously computed value to initialize the cumulative sum of powers.
The special method .__call__()
turns the instances of CumulativePowerFactory
into callable objects. In other words, you can call the instances of CumulativePowerFactory
like you call any regular function.
Inside .__call__()
, you first compute the power of base
raised to exponent
. Then you add the resulting value to the current value of .total
. Finally, you return the computed power.
To give CumulativePowerFactory
a try, open a Python interactive session in the directory containing power.py
and run the following code:
>>> from power import CumulativePowerFactory
>>> square = CumulativePowerFactory()
>>> square(21)
441
>>> square(42)
1764
>>> square.total
2205
>>> cube = CumulativePowerFactory(exponent=3)
>>> cube(21)
9261
>>> cube(42)
74088
>>> cube.total
83349
>>> initialized_cube = CumulativePowerFactory(3, start=2205)
>>> initialized_cube(21)
9261
>>> initialized_cube(42)
74088
>>> initialized_cube.total
85554
These examples show how CumulativePowerFactory
simulates multiple constructors. For example, the first constructor doesn’t take arguments. It allows you to create class instances that compute powers of 2
, which is the default value of the exponent
argument. The .total
instance attribute holds the cumulative sum of computed powers as you go.
The second example shows a constructor that takes exponent
as an argument and returns a callable instance that computes cubes. In this case, .total
works the same as in the first example.
The third example shows how CumulativePowerFactory
seems to have another constructor that allows you to create instances by providing the exponent
and start
arguments. Now .total
starts with a value of 2205
, which initializes the sum of powers.
Using optional arguments when you’re implementing .__init__()
in your classes is a clean and Pythonic technique to create classes that simulate multiple constructors.
Checking Argument Types in .__init__()
Another approach to simulate multiple constructors is to write a .__init__()
method that behaves differently depending on the argument type. To check the type of a variable in Python, you commonly rely on the built-in isinstance()
function. This function returns True
if an object is an instance of a given class and False
otherwise:
>>> isinstance(42, int)
True
>>> isinstance(42, float)
False
>>> isinstance(42, (list, int))
True
>>> isinstance(42, list | int) # Python >= 3.10
True
The first argument to isinstance()
is the object that you want to type check. The second argument is the class or data type of reference. You can also pass a tuple of types to this argument. If you’re running Python 3.10 or later, then you can also use the new union syntax with the pipe symbol (|
).
Now say that you want to continue working on your Person
class, and you need that class to also accept the person’s birth date. Your code will represent the birth date as a date
object, but for convenience your users will also have the option of providing the birth date as a string with a given format. In this case, you can do something like the following:
>>> from datetime import date
>>> class Person:
... def __init__(self, name, birth_date):
... self.name = name
... if isinstance(birth_date, date):
... self.birth_date = birth_date
... elif isinstance(birth_date, str):
... self.birth_date = date.fromisoformat(birth_date)
...
>>> jane = Person("Jane Doe", "2000-11-29")
>>> jane.birth_date
datetime.date(2000, 11, 29)
>>> john = Person("John Doe", date(1998, 5, 15))
>>> john.birth_date
datetime.date(1998, 5, 15)
Inside .__init__()
, you first define the usual .name
attribute. The if
clause of the conditional statement checks if the provided birth date is a date
object. If so, then you define .birth_date
to store the date at hand.
The elif
clause checks if the birth_date
argument is of str
type. If so, then you set .birth_date
to a date
object built from the provided string. Note that the birth_date
argument should be a string with a date in ISO format, YYYY-MM-DD.
That’s it! Now you have a .__init__()
method that simulates a class with multiple constructors. One constructor takes arguments of date
type. The other constructor takes arguments of string type.
Note: If you’re running Python 3.10 or greater, then you can also use the structural pattern matching syntax to implement the technique in this section.
The technique in the above example has the drawback that it doesn’t scale well. If you have multiple arguments that can take values of different data types, then your implementation can soon become a nightmare. So, this technique is considered an anti-pattern in Python.
Note: PEP 443 states that “…it is currently a common anti-pattern for Python code to inspect the types of received arguments, in order to decide what to do with the objects.” This coding pattern is “brittle and closed to extension,” according to the same document.
PEP 443 therefore introduced single-dispatch generic functions to help you avoid using this coding anti-pattern whenever possible. You’ll learn more about this feature in the section Providing Multiple Class Constructors With @singledispatchmethod
.
For example, what would happen if your user input a Unix time value for birth_date
? Check out the following code snippet:
>>> linda = Person("Linda Smith", 1011222000)
>>> linda.birth_date
Traceback (most recent call last):
...
AttributeError: 'Person' object has no attribute 'birth_date'
When you access .birth_date
, you get an AttributeError
because your conditional statement doesn’t have a branch that considers a different date format.
To fix this issue, you can continue adding elif
clauses to cover all the possible date formats that the user can pass. You can also add an else
clause to catch unsupported date formats:
>>> from datetime import date
>>> class Person:
... def __init__(self, name, birth_date):
... self.name = name
... if isinstance(birth_date, date):
... self.birth_date = birth_date
... elif isinstance(birth_date, str):
... self.birth_date = date.fromisoformat(birth_date)
... else:
... raise ValueError(f"unsupported date format: {birth_date}")
...
>>> linda = Person("Linda Smith", 1011222000)
Traceback (most recent call last):
...
ValueError: unsupported date format: 1011222000
In this example, the else
clause runs if the value of birth_date
isn’t a date
object or a string holding a valid ISO date. This way, the exceptional situation doesn’t pass silently.
Providing Multiple Constructors With @classmethod
in Python
A powerful technique for providing multiple constructors in Python is to use @classmethod
. This decorator allows you to turn a regular method into a class method.
Unlike regular methods, class methods don’t take the current instance, self
, as an argument. Instead, they take the class itself, which is commonly passed in as the cls
argument. Using cls
to name this argument is a popular convention in the Python community.
Here’s the basic syntax to define a class method:
>>> class DemoClass:
... @classmethod
... def class_method(cls):
... print(f"A class method from {cls.__name__}!")
...
>>> DemoClass.class_method()
A class method from DemoClass!
>>> demo = DemoClass()
>>> demo.class_method()
A class method from DemoClass!
DemoClass
defines a class method using Python’s built-in @classmethod
decorator. The first argument of .class_method()
holds the class itself. Through this argument, you can access the class from inside itself. In this example, you access the .__name__
attribute, which stores the name of the underlying class as a string.
It’s important to note that you can access a class method using either the class or a concrete instance of the class at hand. No matter how you invoke .class_method()
, it’ll receive DemoClass
as its first argument. The ultimate reason why you can use class methods as constructors is that you don’t need an instance to call a class method.
Using @classmethod
makes it possible to add as many explicit constructors as you need to a given class. It’s a Pythonic and popular way to implement multiple constructors. You can also call this type of constructor an alternative constructor in Python, as Raymond Hettinger does in his PyCon talk Python’s Class Development Toolkit.
Now, how can you use a class method to customize Python’s instantiation process? Instead of fine-tuning .__init__()
and the object initialization, you’ll control both steps: object creation and initialization. Through the following examples, you’ll learn how to do just that.
Constructing a Circle From Its Diameter
To create your first class constructor with @classmethod
, say you’re coding a geometry-related application and need a Circle
class. Initially, you define your class as follows:
# circle.py
import math
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
def __repr__(self):
return f"{self.__class__.__name__}(radius={self.radius})"
The initializer of Circle
takes a radius value as an argument and stores it in an instance attribute called .radius
. Then the class implements methods to compute the circle’s area and perimeter using Python’s math
module. The special method .__repr__()
returns a suitable string representation for your class.
Go ahead and create the circle.py
file in your working directory. Then open the Python interpreter and run the following code to try out Circle
:
>>> from circle import Circle
>>> circle = Circle(42)
>>> circle
Circle(radius=42)
>>> circle.area()
5541.769440932395
>>> circle.perimeter()
263.89378290154264
Cool! Your class works correctly! Now say that you also want to instantiate Circle
using the diameter. You can do something like Circle(diameter / 2)
, but that’s not quite Pythonic or intuitive. It’d be better to have an alternative constructor to create circles by using their diameter directly.
Go ahead and add the following class method to Circle
right after .__init__()
:
# circle.py
import math
class Circle:
def __init__(self, radius):
self.radius = radius
@classmethod
def from_diameter(cls, diameter):
return cls(radius=diameter / 2)
# ...
Here, you define .from_diameter()
as a class method. Its first argument receives a reference to the containing class, Circle
.
The second argument holds the diameter of the specific circle that you want to create. Inside the method, you first calculate the radius using the input value of diameter
. Then you instantiate Circle
by calling cls
with the radius that results from the diameter
argument.
This way, you’re in full control of creating and initializing the instances of Circle
using the diameter as an argument.
Note: In the example above, you can seemingly achieve the same result by calling Circle
itself instead of cls
. However, this can potentially lead to bugs if your class is subclassed. Those subclasses will then call Circle
instead of themselves when they’re initialized with .from_diameter()
.
The call to the cls
argument automatically runs the object creation and initialization steps that Python needs to instantiate a class. Finally, .from_diameter()
returns the new instance to the caller.
Note: A popular convention within the Python community is to use the from
preposition to name constructors that you create as class methods.
Here’s how you can use your brand-new constructor to create circles by using the diameter:
>>> from circle import Circle
>>> Circle.from_diameter(84)
Circle(radius=42.0)
>>> circle.area()
5541.769440932395
>>> circle.perimeter()
263.89378290154264
The call to .from_diameter()
on Circle
returns a new instance of the class. To construct that instance, the method uses the diameter instead of the radius. Note that the rest of the functionality of Circle
works the same as before.
Using @classmethod
as you did in the example above is the most common way to provide explicit multiple constructors in your classes. With this technique, you have the option to select the right name for each alternative constructor that you provide, which can make your code more readable and maintainable.
Building a Polar Point From Cartesian Coordinates
For a more elaborate example of providing multiple constructors using class methods, say you have a class representing a polar point in a math-related application. You need a way to make your class more flexible so that you can construct new instances using Cartesian coordinates as well.
Here’s how you can write a constructor to meet this requirement:
# point.py
import math
class PolarPoint:
def __init__(self, distance, angle):
self.distance = distance
self.angle = angle
@classmethod
def from_cartesian(cls, x, y):
distance = math.dist((0, 0), (x, y))
angle = math.degrees(math.atan2(y, x))
return cls(distance, angle)
def __repr__(self):
return (
f"{self.__class__.__name__}"
f"(distance={self.distance:.1f}, angle={self.angle:.1f})"
)
In this example, .from_cartesian()
takes two arguments representing a given point’s x
and y
Cartesian coordinates. Then the method calculates the required distance
and angle
to construct the corresponding PolarPoint
object. Finally, .from_cartesian()
returns a new instance of the class.
Here’s how the class works, using both coordinate systems:
>>> from point import PolarPoint
>>> # With polar coordinates
>>> PolarPoint(13, 22.6)
PolarPoint(distance=13.0, angle=22.6)
>>> # With cartesian coordinates
>>> PolarPoint.from_cartesian(x=12, y=5)
PolarPoint(distance=13.0, angle=22.6)
In these examples, you use the standard instantiation process and your alternative constructor, .from_cartesian()
, to create PolarPoint
instances using conceptually different initialization arguments.
Exploring Multiple Constructors in Existing Python Classes
Using the @classmethod
decorator to provide multiple constructors in a class is a fairly popular technique in Python. There are several examples of built-in and standard-library classes that use this technique to provide multiple alternative constructors.
In this section, you’ll learn about three of the most notable examples of these classes: dict
, datetime.date
, and pathlib.Path
.
Constructing Dictionaries From Keys
Dictionaries are a fundamental data type in Python. They’re present in every piece of Python code, either explicitly or implicitly. They’re also a cornerstone of the language itself because important parts of the CPython implementation rely on them.
You have several ways to define dictionary instances in Python. You can use dictionary literals, which consist of key-value pairs in curly brackets ({}
). You can also call dict()
explicitly with keyword arguments or with a sequence of two-item tuples, for example.
This popular class also implements an alternative constructor called .fromkeys()
. This class method takes an iterable
of keys and an optional value
. The value
argument defaults to None
and works as the value for all the keys in the resulting dictionary.
Now, how can .fromkeys()
be useful in your code? Say you’re running an animal shelter, and you need to build a small application to track how many animals currently live in your shelter. Your app uses a dictionary to store the inventory of animals.
Because you already know which species you’re capable of housing at the shelter, you can create the initial inventory dictionary dynamically, like in the following code snippet:
>>> allowed_animals = ["dog", "cat", "python", "turtle"]
>>> animal_inventory = dict.fromkeys(allowed_animals, 0)
>>> animal_inventory
{'dog': 0, 'cat': 0, 'python': 0, 'turtle': 0}
In this example, you build an initial dictionary using .fromkeys()
, which takes the keys from allowed_animals
. You set the initial inventory of each animal to 0
by providing this value as the second argument to .fromkeys()
.
As you already learned, value
defaults to None
, which can be a suitable initial value for your dictionary’s keys in some situations. However, in the example above, 0
is a convenient value because you’re working with the number of individuals that you have from each species.
Note: Most of the time, the Counter
class from the collections
module is a more appropriate tool for tackling inventory problems like the one above. However, Counter
doesn’t provide a suitable implementation of .fromkeys()
to prevent ambiguities like Counter.fromkeys("mississippi", 0)
.
Other mappings in the standard library also have a constructor called .fromkeys()
. This is the case with OrderedDict
, defaultdict
, and UserDict
. For example, the source code of UserDict
provides the following implementation of .fromkeys()
:
@classmethod
def fromkeys(cls, iterable, value=None):
d = cls()
for key in iterable:
d[key] = value
return d
Here, .fromkeys()
takes an iterable
and a value
as arguments. The method creates a new dictionary by calling cls
. Then it iterates over the keys in iterable
and sets each value to value
, which defaults to None
, as usual. Finally, the method returns the newly created dictionary.
Creating datetime.date
Objects
The datetime.date
class from the standard library is another class that takes advantage of multiple constructors. This class provides several alternative constructors, such as .today()
, .fromtimestamp()
, .fromordinal()
, and .fromisoformat()
. All of them allow you to construct datetime.date
objects using conceptually different arguments.
Here are a few examples of how to use some of those constructors to create datetime.date
objects:
>>> from datetime import date
>>> from time import time
>>> # Standard constructor
>>> date(2022, 1, 13)
datetime.date(2022, 1, 13)
>>> date.today()
datetime.date(2022, 1, 13)
>>> date.fromtimestamp(1642110000)
datetime.date(2022, 1, 13)
>>> date.fromtimestamp(time())
datetime.date(2022, 1, 13)
>>> date.fromordinal(738168)
datetime.date(2022, 1, 13)
>>> date.fromisoformat("2022-01-13")
datetime.date(2022, 1, 13)
The first example uses the standard class constructor as a reference. The second example shows how you can use .today()
to build a date
object from the current day’s date.
The rest of the examples show how datetime.date
uses several class methods to provide multiple constructors. This variety of constructors makes the instantiation process pretty versatile and powerful, covering a wide range of use cases. It also improves the readability of your code with descriptive method names.
Finding Your Path to Home
Python’s pathlib
module from the standard library provides convenient and modern tools for gracefully handling system paths in your code. If you’ve never used this module, then check out Python’s pathlib Module: Taming the File System.
The handiest tool in pathlib
is its Path
class. This class allows you to handle your system path in a cross-platform way. Path
is another standard-library class that provides multiple constructors. For example, you’ll find Path.home()
, which creates a path object from your home directory:
The .home()
constructor returns a new path object representing the user’s home directory. This alternative constructor can be useful when you’re handling configuration files in your Python applications and projects.
Finally, Path
also provides a constructor called .cwd()
. This method creates a path object from your current working directory. Go ahead and give it a try!
Providing Multiple Constructors With @singledispatchmethod
The final technique that you’ll learn is known as single-dispatch generic functions. With this technique, you can add multiple constructors to your classes and run them selectively, according to the type of their first argument.
A single-dispatch generic function consists of multiple functions implementing the same operation for different data types. The underlying dispatch algorithm determines which implementation to run based on the type of a single argument. That’s where the term single dispatch comes from.
Starting with Python 3.8, you can use the @singledispatch
or @singledispatchmethod
decorator to turn a function or a method, respectively, into a single-dispatch generic function. PEP 443 explains that you can find these decorators in the functools
module.
In a regular function, Python selects the implementation to dispatch according to the type of the function’s first argument. In a method, the target argument is the first one immediately after self
.
A Demo Example of a Single-Dispatch Method
To apply the single-dispatch method technique to a given class, you need to define a base method implementation and decorate it with @singledispatchmethod
. Then you can write alternative implementations and decorate them using the name of the base method plus .register
.
Here’s an example that shows the basic syntax:
# demo.py
from functools import singledispatchmethod
class DemoClass:
@singledispatchmethod
def generic_method(self, arg):
print(f"Do something with argument of type: {type(arg).__name__}")
@generic_method.register
def _(self, arg: int):
print("Implementation for an int argument...")
@generic_method.register(str)
def _(self, arg):
print("Implementation for a str argument...")
In DemoClass
, you first define a base method called generic_method()
and decorate it with @singledispatchmethod
. Then you define two alternative implementations of generic_method()
and decorate them with @generic_method.register
.
In this example, you name the alternative implementations using a single underscore (_
) as a placeholder name. In real code, you should use descriptive names, provided that they’re different from the base method name, generic_method()
. When using descriptive names, consider adding a leading underscore to mark the alternative methods as non-public and prevent direct calls from your end users.
You can use type annotations to define the type of the target argument. You can also explicitly pass the type of the target argument to the .register()
decorator. If you need to define a method to process several types, then you can stack multiple calls to .register()
, with the required type for each.
Here’s how your class works:
>>> from demo import DemoClass
>>> demo = DemoClass()
>>> demo.generic_method(42)
Implementation for an int argument...
>>> demo.generic_method("Hello, World!")
Implementation for a str argument...
>>> demo.generic_method([1, 2, 3])
Do something with argument of type: list
If you call .generic_method()
with an integer number as an argument, then Python runs the implementation corresponding to the int
type. Similarly, when you call the method with a string, Python dispatches the string implementation. Finally, if you call .generic_method()
with an unregistered data type, such as a list, then Python runs the base implementation of the method.
You can also use this technique to overload .__init__()
, which will allow you to provide multiple implementations for this method, and therefore, your class will have multiple constructors.
A Real-World Example of a Single-Dispatch Method
As a more realistic example of using @singledispatchmethod
, say you need to continue adding features to your Person
class. This time, you need to provide a way to compute the approximate age of a person based on their birth date. To add this feature to Person
, you can use a helper class that handles all the information related to the birth date and age.
Go ahead and create a file called person.py
in your working directory. Then add the following code to it:
1# person.py
2
3from datetime import date
4from functools import singledispatchmethod
5
6class BirthInfo:
7 @singledispatchmethod
8 def __init__(self, birth_date):
9 raise ValueError(f"unsupported date format: {birth_date}")
10
11 @__init__.register(date)
12 def _from_date(self, birth_date):
13 self.date = birth_date
14
15 @__init__.register(str)
16 def _from_isoformat(self, birth_date):
17 self.date = date.fromisoformat(birth_date)
18
19 @__init__.register(int)
20 @__init__.register(float)
21 def _from_timestamp(self, birth_date):
22 self.date = date.fromtimestamp(birth_date)
23
24 def age(self):
25 return date.today().year - self.date.year
Here’s a breakdown of how this code works:
-
Line 3 imports
date
fromdatetime
so that you can later convert any input date to adate
object. -
Line 4 imports
@singledispatchmethod
to define the overloaded method. -
Line 6 defines
BirthInfo
as a regular Python class. -
Lines 7 to 9 define the class initializer as a single-dispatch generic method using
@singledispatchmethod
. This is the method’s base implementation, and it raises aValueError
for unsupported date formats. -
Lines 11 to 13 register an implementation of
.__init__()
that processesdate
objects directly. -
Lines 15 to 17 define the implementation of
.__init__()
that processes dates that come as strings with an ISO format. -
Lines 19 to 22 register an implementation that processes dates that come as Unix time in seconds since the epoch. This time, you register two instances of the overloaded method by stacking the
.register
decorator with theint
andfloat
types. -
Lines 24 to 25 provide a regular method to compute the age of a given person. Note that the implementation of
age()
isn’t totally accurate because it doesn’t consider the month and day of the year when calculating the age. Theage()
method is just a bonus feature to enrich the example.
Now you can use composition in your Person
class to take advantage of the new BirthInfo
class. Go ahead and update Person
with the following code:
# person.py
# ...
class Person:
def __init__(self, name, birth_date):
self.name = name
self._birth_info = BirthInfo(birth_date)
@property
def age(self):
return self._birth_info.age()
@property
def birth_date(self):
return self._birth_info.date
In this update, Person
has a new non-public attribute called ._birth_info
, which is an instance of BirthInfo
. This instance is initialized with the input argument birth_date
. The overloaded initializer of BirthInfo
will initialize ._birth_info
according to the user’s birth date.
Then you define age()
as a property to provide a computed attribute that returns the person’s current approximate age. The final addition to Person
is the birth_date()
property, which returns the person’s birth date as a date
object.
To try out your Person
and BirthInfo
classes, open an interactive session and run the following code:
>>> from person import Person
>>> john = Person("John Doe", date(1998, 5, 15))
>>> john.age
24
>>> john.birth_date
datetime.date(1998, 5, 15)
>>> jane = Person("Jane Doe", "2000-11-29")
>>> jane.age
22
>>> jane.birth_date
datetime.date(2000, 11, 29)
>>> linda = Person("Linda Smith", 1011222000)
>>> linda.age
20
>>> linda.birth_date
datetime.date(2002, 1, 17)
>>> david = Person("David Smith", {"year": 2000, "month": 7, "day": 25})
Traceback (most recent call last):
...
ValueError: unsupported date format: {'year': 2000, 'month': 7, 'day': 25}
You can instantiate Person
using different date formats. The internal instance of BirthDate
automatically converts the input date into a date object. If you instantiate Person
with an unsupported date format, such as a dictionary, then you get a ValueError
.
Note that BirthDate.__init__()
takes care of processing the input birth date for you. There’s no need to use explicit alternative constructors to process different types of input. You can just instantiate the class using the standard constructor.
The main limitation of the single-dispatch method technique is that it relies on a single argument, the first argument after self
. If you need to use multiple arguments for dispatching appropriate implementations, then check out some existing third-party libraries, such as multipledispatch and multimethod.
Conclusion
Writing Python classes with multiple constructors can make your code more versatile and flexible, covering a wide range of use cases. Multiple constructors are a powerful feature that allows you to build instances of the underlying class using arguments of different types, a different number of arguments, or both, depending on your needs.
In this tutorial, you learned how to:
- Simulate multiple constructors using optional arguments and type checking
- Write multiple constructors using the built-in
@classmethod
decorator - Overload your class constructors using the
@singledispatchmethod
decorator
You also learned how Python internally constructs instances of a given class and how some standard-library classes provide multiple constructors.
With this knowledge, now you can spice up your classes with multiple constructors, equipping them with several ways to tackle the instantiation process 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.
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 Multiple Constructors in Your Python Classes