Locked learning resources

Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Locked learning resources

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Subclassing Immutable Built-in Types

If you’re interested in converting units, then you can check out Pint.

00:00 Subclassing Immutable Built-in Types. Let’s start with a use case of .__new__() that consists of subclassing an immutable built-in type. As an example, let’s say you need to write a Distance class as a subclass of Python’s float type.

00:17 Your class will have an additional attribute to store the unit that’s used to measure the distance. Here’s a first approach to this problem, using the .__init__() method.

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

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

01:24 Here, .__new__() runs the three steps that you learned in the previous section. First, the method creates a new of the current class, cls, by calling super().__new__().

01:35 This time, the call rolls back to float.__new__(), which creates a new instance and initializes it using value as an argument.

01:43 Then the method customizes the new instance by adding a .unit attribute to it. Finally, the new instance gets returned.

01:53 Now your Distance class works as expected, allowing you to use an instance attribute for storing the unit in which you’re measuring the distance.

02:01 Unlike the floating-point value stored in a given instance of Distance, the .unit attribute is mutable, so you can change its value any time you like.

02:10 Finally, note how a call to the dir() function reveals that your class inherits features and methods from float.

02:22 Note that the Distance class in this example doesn’t provide a proper unit conversion mechanism. This means that something like Distance(10, "km") + Distance(20, "miles") won’t attempt at converting units before adding the values.

02:39 If you’re interested in converting units, then check out this project on PyPI. Having seen how to subclass a built-in, in the next section of the course, you’ll see how a class can return an instance of a different class.

Avatar image for raaki88

raaki88 on Sept. 29, 2022

Hi Darren,

Thank you for the course, one question , you did say that new will accept only one argument for class, how come we can pass a value as well in this case ?

Avatar image for praghavan1973

praghavan1973 on June 13, 2023

In response to question from raaki88:

I think the new for float accepts only one argument which is the value for float. For the new subclass we are creating we can accept depending on our definition of new.

Avatar image for mochibeepboop

mochibeepboop on Sept. 18, 2023

Why do we need to overrride new just to create a simple class that calculates distance?

Avatar image for Bartosz Zaczyński

Bartosz Zaczyński RP Team on Sept. 18, 2023

@mochibeepboop Strictly speaking, you don’t have to override the .__new__() method to implement a class that calculates distance. For example, you could’ve implemented the same logic as a data class, avoiding inheritance and many challenges associated with it:

>>> from dataclasses import dataclass

>>> @dataclass
... class Distance:
...     value: float
...     unit: str
...     
...     def __add__(self, other):
...         if isinstance(other, Distance):
...             if self.unit == other.unit:
...                 return Distance(self.value + other.value, self.unit)
...         raise NotImplementedError
...
>>> d1 = Distance(42.0, "miles")
>>> d2 = Distance(8.0, "miles")

>>> d1 + d2
Distance(value=50.0, unit='miles')

Darren’s code serves as an example to demonstrate how you can subclass a built-in immutable data type, such as float. Notice that floating-point numbers take one argument—the value—and you want your Distance class to take an additional unit argument. When you override .__init__() by adding this extra argument, instantiating the class results in an error:

>>> class Distance(float):
...     def __init__(self, value, unit):
...         super().__init__(value)
...         self.unit = unit
...
>>> Distance(42.0, "miles")
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    Distance(42.0, "miles")
TypeError: float expected at most 1 argument, got 2

That’s because super().__init__() still expects only the value argument. Therefore, you must override the .__new__() method in such a way that it accepts both the value and unit arguments but only passes the value to super().__new__():

>>> class Distance(float):
...     def __new__(cls, value, unit):
...         instance = super().__new__(cls, value)
...         instance.unit = unit
...         return instance
...
>>> Distance(42.0, "miles")
42.0
>>> distance = Distance(42.0, "miles")
>>> distance.unit
'miles'

Become a Member to join the conversation.