Providing Multiple Constructors in Your Python Classes

Providing Multiple Constructors in Your Python Classes

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: Providing 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.

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:

Python
>>> # 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.

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:

Python
>>> # 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:

  1. Create a new instance of the target class.
  2. 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.

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.

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:

Python
# 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:

Python
>>> 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:

Python
>>> 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.

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:

Python
# 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:

Python
>>> 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:

Python
>>> 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:

Python
>>> 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.

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.

For example, what would happen if your user input a Unix time value for birth_date? Check out the following code snippet:

Python
>>> 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:

Python
>>> 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:

Python
>>> 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:

Python
# 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:

Python
>>> 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__():

Python
# 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.

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.

Here’s how you can use your brand-new constructor to create circles by using the diameter:

Python
>>> 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:

Python
# 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:

Python
>>> 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:

Python
>>> 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.

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():

Python
@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:

Python
>>> 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 3’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:

Python
>>> from pathlib import Path

>>> Path.home()
WindowsPath('C:/Users/username')
Python
>>> from pathlib import Path

>>> Path.home()
PosixPath('/home/username')

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:

Python
# 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:

Python
>>> 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:

Python
 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 from datetime so that you can later convert any input date to a date 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 a ValueError for unsupported date formats.

  • Lines 11 to 13 register an implementation of .__init__() that processes date 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 the int and float 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. The age() 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:

Python
# 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:

Python
>>> 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.

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: Providing Multiple Constructors in Your Python Classes

🐍 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

Locked learning resources

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

Locked learning resources

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:

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: Providing Multiple Constructors in Your Python Classes