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.
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.
mochibeepboop on Sept. 18, 2023
Why do we need to overrride new just to create a simple class that calculates distance?
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'
Andras on Jan. 6, 2025
Hi Bartosz, I am still a bit confused about your claim above:
”…but only passes the value to super().__new__()
”.
It actually passes both the class Distance
in cls
and the value as two positional parameters. But why would you want to implement float.__new()__
to expect a parameter other than the actual value? What has to happen, obviously, is that instance
is a float instance with the value but its type is somehow tweaked/changed to Distance
. So by the time the assignment is done, instance
is already of type Distance
.
Is it fair to say that when implementing float.__new__
the developers designed it such that it can return an object of any type?
Andras on Jan. 6, 2025
ah okay, re-reading the sentence makes it clear(er): out of unit
and value
you indeed only pass value
. Nevertheless, my question about float.__new__
still holds I think.
Thanks in advance!
Bartosz Zaczyński RP Team on Jan. 7, 2025
Is it fair to say that when implementing
float.__new__
the developers designed it such that it can return an object of any type?
@Andras That’s true for all types in Python. You can override the .__new__()
method in such a way that it’ll return an object of a completely different type. There are many reasons why you’d want to do that, including caching, implementing the singleton pattern, or mocking, just to name a few.
Andras on Jan. 10, 2025
Thank you Bartosz, I think I understand it now, at least not so reluctant to accept it.
So float
derives from object
but overrides object.__new__()
(that only accepts a single cls
parameter) such that it can take both cls
(Distance in the example) and value
. In fact its signature is even more generic; just checked what REPL gives for float.__new__
:
<function float.__new__(*args, **kwargs)>
I am all good, many thanks!
Bartosz Zaczyński RP Team on Jan. 10, 2025
@Andras You’re very welcome! I’m glad it makes sense now 🙂 Let us know if you have more questions.
Become a Member to join the conversation.
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 ?