Recommended Video Course
Using Multiple Constructors in Your Python Classes
Table of Contents
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
Python doesn’t support constructor overloading in the same way that Java or C++ do. However, you can simulate multiple constructors by defining default arguments in.__init__()
and use@classmethod
to define alternative constructors. Another option is to employ the@singledispatchmethod
decorator for method overloading based on argument types. These techniques provide flexible ways to construct objects in Python.
By the end of this tutorial, you’ll understand that:
.__init__()
method.@classmethod
decorator allows you todefine alternative constructors, using the class itself as the first argument.@singledispatchmethod
can simulate overloaded constructors based on thetype of the first argument.Having the tools to provide multiple constructors will help you write flexible classes that can adapt to changing needs.
You’ll also get a peek under the hood at how Python internallyconstructs instances of a regular class and how somestandard-library classes provide multiple constructors.
To get the most out of this tutorial, you should have basic knowledge ofobject-oriented programming and understand how to defineclass methods with@classmethod
. You should also have experience working withdecorators 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.
Python supportsobject-oriented programming withclasses that are straightforward to create and use. Python classes offer powerful features that can help you write better software. Classes are like blueprints forobjects, also known asinstances. 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 theclass
keyword followed by the class name:
>>># Define a Person class>>>classPerson:...def__init__(self,name):...self.name=name...
Python has a rich set ofspecial 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 theinstance initializer in Python. This method’s job is to initializeinstance attributes with appropriate values when you instantiate a given class.
InPerson
, the.__init__()
method’s first argument is calledself
. This argument holds the current object or instance, which is passed implicitly in the method call. This argument is common to everyinstance method in Python. The second argument to.__init__()
is calledname
and will hold the person’s name as astring.
Note: Usingself
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 startinstantiating 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 Pythonfunction:
>>># Instantiating Person>>>john=Person("John Doe")>>>john.name'John Doe'
In Python, the class name provides what other languages, such asC++ andJava, call theclass constructor. Calling a class, like you did withPerson
, triggers Python’s classinstantiation process, which internally runs in two steps:
To continue with the above example, the value that you pass as an argument toPerson
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 aclass constructor in Python. However, its job is actually to create new objects from the class blueprint, so you can more accurately call it aninstance creator orobject 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’sobject
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 providingmultiple constructors in your classes. In other words, you’ll provide multiple ways to construct objects of a givenPython class.
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 asC++,C#, andJava, support what is known asfunction 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 internaldictionary called.__dict__
, which holds the classnamespace. 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
classGreeter:defsay_hello(self):print("Hello, World")defsay_hello(self):print("Hello, Pythonista")
In this example, you createGreeter
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 agreet.py
file in your working directory and run the following code in aninteractive session:
>>>fromgreetimportGreeter>>>greeter=Greeter()>>>greeter.say_hello()Hello, Pythonista>>>Greeter.__dict__mappingproxy({..., 'say_hello': <function Greeter.say_hello at...>, ...})
In this example, you call.say_hello()
ongreeter
, which is an instance of theGreeter
class. You getHello, Pythonista
instead ofHello, 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:
>>>defsay_hello():...print("Hello, World")...>>>defsay_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 getHello, 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 ismultiple 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 thestandard 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 passingoptional arguments and by checking the argument types to determine different behaviors in your instance initializers.
A pretty useful technique for simulating multiple constructors in a Python class is to provide.__init__()
withoptional arguments usingdefault 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 thedata 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__()
.
.__init__()
An elegant and Pythonic way to simulate multiple constructors is to implement a.__init__()
method withoptional arguments. You can do this by specifying appropriatedefault argument values.
Note: You can also provide optional arguments in your functions and methods using an undefined number ofpositional arguments or an undefined number ofkeyword arguments. Check outUsing Python Optional Arguments When Defining Functions for more details on these options.
To this end, say you need to code afactory class calledCumulativePowerFactory
. 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 apower.py
file in your current directory. Then type in the following code to implementCumulativePowerFactory
:
power.py
classCumulativePowerFactory:def__init__(self,exponent=2,*,start=0):self._exponent=exponentself.total=startdef__call__(self,base):power=base**self._exponentself.total+=powerreturnpower
The initializer ofCumulativePowerFactory
takes two optional arguments,exponent
andstart
. The first argument holds the exponent that you’ll use to compute a series of powers. It defaults to2
, which is a commonly used value when it comes to computing powers.
The star or asterisk symbol (*
) afterexponent
means thatstart
is akeyword-only argument. To pass a value to a keyword-only argument, you need to use the argument’s name explicitly. In other words, to setarg
tovalue
, you need to explicitly typearg=value
.
Thestart
argument holds the initial value to compute the cumulative sum of powers. It defaults to0
, 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 ofCumulativePowerFactory
intocallable objects. In other words, you can call the instances ofCumulativePowerFactory
like you call any regular function.
Inside.__call__()
, you first compute the power ofbase
raised toexponent
. Then you add the resulting value to the current value of.total
. Finally, youreturn the computed power.
To giveCumulativePowerFactory
a try, open a Pythoninteractive session in the directory containingpower.py
and run the following code:
>>>frompowerimportCumulativePowerFactory>>>square=CumulativePowerFactory()>>>square(21)441>>>square(42)1764>>>square.total2205>>>cube=CumulativePowerFactory(exponent=3)>>>cube(21)9261>>>cube(42)74088>>>cube.total83349>>>initialized_cube=CumulativePowerFactory(3,start=2205)>>>initialized_cube(21)9261>>>initialized_cube(42)74088>>>initialized_cube.total85554
These examples show howCumulativePowerFactory
simulates multiple constructors. For example, the first constructor doesn’t take arguments. It allows you to create class instances that compute powers of2
, which is the default value of theexponent
argument. The.total
instance attribute holds the cumulative sum of computed powers as you go.
The second example shows a constructor that takesexponent
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 howCumulativePowerFactory
seems to have another constructor that allows you to create instances by providing theexponent
andstart
arguments. Now.total
starts with a value of2205
, 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.
.__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 avariable in Python, you commonly rely on the built-inisinstance()
function. This function returnsTrue
if an object is an instance of a given class andFalse
otherwise:
>>>isinstance(42,int)True>>>isinstance(42,float)False>>>isinstance(42,(list,int))True>>>isinstance(42,list|int)# Python >= 3.10True
The first argument toisinstance()
is the object that you want to type check. The second argument is the class or data type of reference. You can also pass atuple of types to this argument. If you’re runningPython 3.10 or later, then you can also use the newunion syntax with the pipe symbol (|
).
Now say that you want to continue working on yourPerson
class, and you need that class to also accept the person’s birth date. Your code will represent the birth date as adate
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:
>>>fromdatetimeimportdate>>>classPerson:...def__init__(self,name,birth_date):...self.name=name...ifisinstance(birth_date,date):...self.birth_date=birth_date...elifisinstance(birth_date,str):...self.birth_date=date.fromisoformat(birth_date)...>>>jane=Person("Jane Doe","2000-11-29")>>>jane.birth_datedatetime.date(2000, 11, 29)>>>john=Person("John Doe",date(1998,5,15))>>>john.birth_datedatetime.date(1998, 5, 15)
Inside.__init__()
, you first define the usual.name
attribute. Theif
clause of theconditional statement checks if the provided birth date is adate
object. If so, then you define.birth_date
to store the date at hand.
Theelif
clause checks if thebirth_date
argument is ofstr
type. If so, then you set.birth_date
to adate
object built from the provided string. Note that thebirth_date
argument should be a string with a date inISO format,YYYY-MM-DD.
That’s it! Now you have a.__init__()
method that simulates a class with multiple constructors. One constructor takes arguments ofdate
type. The other constructor takes arguments of string type.
Note: If you’re runningPython 3.10 or greater, then you can also use thestructural 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 introducedsingle-dispatchgeneric functions to help you avoid using this coding anti-pattern whenever possible. You’ll learn more about this feature in the sectionProviding Multiple Class Constructors With@singledispatchmethod
.
For example, what would happen if your user input aUnix time value forbirth_date
? Check out the following code snippet:
>>>linda=Person("Linda Smith",1011222000)>>>linda.birth_dateTraceback (most recent call last):...AttributeError:'Person' object has no attribute 'birth_date'
When you access.birth_date
, you get anAttributeError
because your conditional statement doesn’t have a branch that considers a different date format.
To fix this issue, you can continue addingelif
clauses to cover all the possible date formats that the user can pass. You can also add anelse
clause to catchunsupported date formats:
>>>fromdatetimeimportdate>>>classPerson:...def__init__(self,name,birth_date):...self.name=name...ifisinstance(birth_date,date):...self.birth_date=birth_date...elifisinstance(birth_date,str):...self.birth_date=date.fromisoformat(birth_date)...else:...raiseValueError(f"unsupported date format:{birth_date}")...>>>linda=Person("Linda Smith",1011222000)Traceback (most recent call last):...ValueError:unsupported date format: 1011222000
In this example, theelse
clause runs if the value ofbirth_date
isn’t adate
object or a string holding a valid ISO date. This way, the exceptional situation doesn’tpass silently.
@classmethod
in PythonA powerful technique for providing multiple constructors in Python is to use@classmethod
. This decorator allows you to turn a regular method into aclass 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 thecls
argument. Usingcls
to name this argument is a popular convention in the Python community.
Here’s the basic syntax to define a class method:
>>>classDemoClass:...@classmethod...defclass_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 astring.
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 receiveDemoClass
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 analternative constructor in Python, asRaymond Hettinger does in his PyCon talkPython’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.
To create your first class constructor with@classmethod
, say you’re coding a geometry-related application and need aCircle
class. Initially, you define your class as follows:
circle.py
importmathclassCircle:def__init__(self,radius):self.radius=radiusdefarea(self):returnmath.pi*self.radius**2defperimeter(self):return2*math.pi*self.radiusdef__repr__(self):returnf"{self.__class__.__name__}(radius={self.radius})"
The initializer ofCircle
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’smath
module. The special method.__repr__()
returns a suitablestring representation for your class.
Go ahead and create thecircle.py
file in your working directory. Then open the Python interpreter and run the following code to try outCircle
:
>>>fromcircleimportCircle>>>circle=Circle(42)>>>circleCircle(radius=42)>>>circle.area()5541.769440932395>>>circle.perimeter()263.89378290154264
Cool! Your class works correctly! Now say that you also want to instantiateCircle
using the diameter. You can do something likeCircle(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 toCircle
right after.__init__()
:
circle.py
importmathclassCircle:def__init__(self,radius):self.radius=radius@classmethoddeffrom_diameter(cls,diameter):returncls(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 ofdiameter
. Then you instantiateCircle
by callingcls
with the radius that results from thediameter
argument.
This way, you’re in full control of creating and initializing the instances ofCircle
using the diameter as an argument.
Note: In the example above, you can seemingly achieve the same result by callingCircle
itself instead ofcls
. However, this can potentially lead to bugs if your class is subclassed. Those subclasses will then callCircle
instead of themselves when they’re initialized with.from_diameter()
.
The call to thecls
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 thefrom
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:
>>>fromcircleimportCircle>>>circle=Circle.from_diameter(84)>>>circleCircle(radius=42.0)>>>circle.area()5541.769440932395>>>circle.perimeter()263.89378290154264
The call to.from_diameter()
onCircle
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 ofCircle
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.
For a more elaborate example of providing multiple constructors using class methods, say you have a class representing apolar point in a math-related application. You need a way to make your class more flexible so that you can construct new instances usingCartesian coordinates as well.
Here’s how you can write a constructor to meet this requirement:
point.py
importmathclassPolarPoint:def__init__(self,distance,angle):self.distance=distanceself.angle=angle@classmethoddeffrom_cartesian(cls,x,y):distance=math.dist((0,0),(x,y))angle=math.degrees(math.atan2(y,x))returncls(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’sx
andy
Cartesian coordinates. Then the method calculates the requireddistance
andangle
to construct the correspondingPolarPoint
object. Finally,.from_cartesian()
returns a new instance of the class.
Here’s how the class works, using both coordinate systems:
>>>frompointimportPolarPoint>>># 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 createPolarPoint
instances using conceptually different initialization arguments.
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
, andpathlib.Path
.
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 theCPython implementation rely on them.
You have several ways todefine dictionary instances in Python. You can use dictionary literals, which consist of key-value pairs in curly brackets ({}
). You can also calldict()
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 aniterable
of keys and an optionalvalue
. Thevalue
argument defaults toNone
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 fromallowed_animals
. You set the initial inventory of each animal to0
by providing this value as the second argument to.fromkeys()
.
As you already learned,value
defaults toNone
, 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, theCounter
class from thecollections
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 likeCounter.fromkeys("mississippi", 0)
.
Othermappings in the standard library also have a constructor called.fromkeys()
. This is the case withOrderedDict
,defaultdict
, andUserDict
. For example, thesource code ofUserDict
provides the following implementation of.fromkeys()
:
@classmethoddeffromkeys(cls,iterable,value=None):d=cls()forkeyiniterable:d[key]=valuereturnd
Here,.fromkeys()
takes aniterable
and avalue
as arguments. The method creates a new dictionary by callingcls
. Then it iterates over the keys initerable
and sets each value tovalue
, which defaults toNone
, as usual. Finally, the method returns the newly created dictionary.
datetime.date
ObjectsThedatetime.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 constructdatetime.date
objects using conceptually different arguments.
Here are a few examples of how to use some of those constructors to createdatetime.date
objects:
>>>fromdatetimeimportdate>>>fromtimeimporttime>>># 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 adate
object from the current day’s date.
The rest of the examples show howdatetime.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.
Python’spathlib
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 outPython’s pathlib Module: Taming the File System.
The handiest tool inpathlib
is itsPath
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 findPath.home()
, which creates a path object from your home directory:
>>>frompathlibimportPath>>>Path.home()WindowsPath('C:/Users/username')
>>>frompathlibimportPath>>>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 handlingconfiguration 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!
@singledispatchmethod
The final technique that you’ll learn is known assingle-dispatchgeneric 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 underlyingdispatch algorithm determines which implementation to run based on the type of a single argument. That’s where the termsingle dispatch comes from.
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 thefunctools
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 afterself
.
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
fromfunctoolsimportsingledispatchmethodclassDemoClass:@singledispatchmethoddefgeneric_method(self,arg):print(f"Do something with argument of type:{type(arg).__name__}")@generic_method.registerdef_(self,arg:int):print("Implementation for an int argument...")@generic_method.register(str)def_(self,arg):print("Implementation for a str argument...")
InDemoClass
, you first define a base method calledgeneric_method()
and decorate it with@singledispatchmethod
. Then you define two alternative implementations ofgeneric_method()
and decorate them with@generic_method.register
.
In this example, you name the alternative implementations using a singleunderscore (_
) 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 asnon-public and prevent direct calls from your end users.
You can usetype 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:
>>>fromdemoimportDemoClass>>>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 theint
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.
As a more realistic example of using@singledispatchmethod
, say you need to continue adding features to yourPerson
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 toPerson
, you can use a helper class that handles all the information related to the birth date and age.
Go ahead and create a file calledperson.py
in your working directory. Then add the following code to it:
person.py
1fromdatetimeimportdate 2fromfunctoolsimportsingledispatchmethod 3 4classBirthInfo: 5@singledispatchmethod 6def__init__(self,birth_date): 7raiseValueError(f"unsupported date format:{birth_date}") 8 9@__init__.register(date)10def_from_date(self,birth_date):11self.date=birth_date1213@__init__.register(str)14def_from_isoformat(self,birth_date):15self.date=date.fromisoformat(birth_date)1617@__init__.register(int)18@__init__.register(float)19def_from_timestamp(self,birth_date):20self.date=date.fromtimestamp(birth_date)2122defage(self):23returndate.today().year-self.date.year
Here’s a breakdown of how this code works:
Line 1 importsdate
fromdatetime
so that you can later convert any input date to adate
object.
Line 2 imports@singledispatchmethod
to define the overloaded method.
Line 4 definesBirthInfo
as a regular Python class.
Lines 5 to 7 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 9 to 11 register an implementation of.__init__()
that processesdate
objects directly.
Lines 13 to 15 define the implementation of.__init__()
that processes dates that come as strings with an ISO format.
Lines 17 to 20 register an implementation that processes dates that come as Unix time in seconds since theepoch. This time, you register two instances of the overloaded method by stacking the.register
decorator with theint
andfloat
types.
Lines 22 to 23 provide a regular method to compute the age of a given person. Note that the implementation ofage()
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 usecomposition in yourPerson
class to take advantage of the newBirthInfo
class. Go ahead and updatePerson
with the following code:
person.py
# ...classPerson:def__init__(self,name,birth_date):self.name=nameself._birth_info=BirthInfo(birth_date)@propertydefage(self):returnself._birth_info.age()@propertydefbirth_date(self):returnself._birth_info.date
In this update,Person
has a new non-public attribute called._birth_info
, which is an instance ofBirthInfo
. This instance is initialized with the input argumentbirth_date
. The overloaded initializer ofBirthInfo
will initialize._birth_info
according to the user’s birth date.
Then you defineage()
as aproperty to provide a computed attribute that returns the person’s current approximate age. The final addition toPerson
is thebirth_date()
property, which returns the person’s birth date as adate
object.
To try out yourPerson
andBirthInfo
classes, open an interactive session and run the following code:
>>>frompersonimportPerson>>>john=Person("John Doe",date(1998,5,15))>>>john.age24>>>john.birth_datedatetime.date(1998, 5, 15)>>>jane=Person("Jane Doe","2000-11-29")>>>jane.age22>>>jane.birth_datedatetime.date(2000, 11, 29)>>>linda=Person("Linda Smith",1011222000)>>>linda.age20>>>linda.birth_datedatetime.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 instantiatePerson
using different date formats. The internal instance ofBirthDate
automatically converts the input date into a date object. If you instantiatePerson
with an unsupported date format, such as a dictionary, then you get aValueError
.
Note thatBirthDate.__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 afterself
. If you need to use multiple arguments for dispatching appropriate implementations, then check out some existing third-party libraries, such asmultipledispatch andmultimethod.
Writing Python classes withmultiple 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:
@classmethod
decorator@singledispatchmethod
decoratorYou also learned how Python internallyconstructs instances of a given class and how somestandard-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.
Now that you have some experience with multiple constructors in Python, you can use the questions and answers below to check your understanding and recap what you’ve learned.
These FAQs are related to the most important concepts you’ve covered in this tutorial. Click theShow/Hide toggle beside each question to reveal the answer.
Python doesn’t support traditional method overloading directly, but you can simulate overloaded constructors using techniques like optional arguments with branching logic in a single.__init__()
method, or by utilizing class methods with different parameter sets.
You can create multiple constructors in Python by using techniques like optional arguments, class methods with@classmethod
, and single-dispatch methods with@singledispatchmethod
.
Because Python allows you to define default values for parameters, you can accept different combinations of arguments in a single.__init__()
method. By checking which arguments were provided, you can branch your object initialization logic accordingly.
You can use@classmethod
to define alternative constructors in a class. Alternative constructors, or factory methods, are methods that return an instance of the class—like.__init__()
does—but do so in a way that can be more flexible or more descriptive than the normal constructor.
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
🐍 Python Tricks 💌
Get a short & sweetPython Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.
AboutLeodanis Pozo Ramos
Leodanis is a self-taught Python developer, educator, and technical writer with over 10 years of experience.
» More about LeodanisMasterReal-World Python Skills With Unlimited Access to Real Python
Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:
MasterReal-World Python Skills
With Unlimited Access to Real Python
Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:
What Do You Think?
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 andget answers to common questions in our support portal.
Keep Learning
Related Topics:intermediatepython
Recommended Video Course:Using Multiple Constructors in Your Python Classes
Related Tutorials:
Already have an account?Sign-In
Almost there! Complete this form and click the button below to gain instant access:
Object-Oriented Programming in Python: The 7 Best Resources (A Free PDF Cheat Sheet)