How to Create a Transparent Attribute Alias in Python2021-10-13
When dealing with evolvng API’s, it may be useful to rename an attribute in a class, but keep the old name around for backwards compatibility. This would mean making one attribute an alias for another. In this post we’ll look at two ways to achieve this.
One way to achieve this is with
@property, wrapping getter and setter methods that forward to the underlying attribute.
For example, here’s a
Widget class where
cycles is an alias for
class Widget: def __init__(self, rotations: int) -> None: self.rotations = rotations @property def cycles(self) -> int: return self.rotations @cycles.setter def cycles(self, value: int) -> None: self.rotations = value
Checking this out with
ipython -i example.py:
In : widget = Widget(1337) In : widget.cycles Out: 1337 In : widget.cycles = 9001 In : widget.cycles Out: 9001 In : widget.rotations Out: 9001
Well, that does the job. We could end the post here... but we won’t.
(A completely transparent alias would also have a deleter method to handle the rarely used
The downside of this approach is that it’s repetitive. If we want many aliases, we have to write several lines of similar property functions for each one. It feels like we’re trapped in Java land, wasting away our years writing simple getters and setters. And then if we want 100% coverage in our tests, we have to independently test each method.
Does Python provide a mechanism to avoid this copypasta? Yes, yes it does.
We can use a descriptor to implement aliasing. Descriptors are a special protocol that can run extra processing during attribute access. This uses through three special methods:
__get__to intercept access
__set__to intercept assignment
__delete__to intercept deletion
Even if you haven’t created a descriptor, if you’ve used Python for a while, you sure as heck have used one.
We even used one above.
The descriptor protocol powers
@property, along with
@cached_property, and more.
(For more on descriptors see the Python docs’ great Descriptor HowTo Guide.)
We can create an alias descriptor class like so:
class Alias: def __init__(self, source_name): self.source_name = source_name def __get__(self, obj, objtype=None): if obj is None: # Class lookup, return descriptor return self return getattr(obj, self.source_name) def __set__(self, obj, value): setattr(obj, self.source_name, value)
source_name is the name of the attribute to alias, which we store inside the descriptor instance.
__set__ methods then proxy getting/setting the underlying attribute on the instance (
__get__ may also be called on the class, in which case we conventionally return the descriptor.
(Again, to be complete we would also handle attribute deletion by adding a
We can use our descriptor like so:
class Widget: def __init__(self, rotations: int) -> None: self.rotations = rotations cycles = Alias("rotations") turns = Alias("rotations")
turns is an instance of our descriptor class.
When touching an alias attribute on the
Widget class, or instances thereof, Python sees that they are descriptors and runs the appropriate method.
In action, this looks similar to the above:
In : widget = Widget(1024) In : widget.turns Out: 1024 In : widget.cycles Out: 1024 In : widget.turns = 2048 In : widget.rotations Out: 2048 In : widget.cycles Out: 2048 In : widget.turns Out: 2048
This approach saves us many lines of code.
And we can test our
Alias class once in isolation, rather than writing many tests for each alias.
But there is one downside.
Astute readers will have noticed that we’ve used type hints in every example except for our
This is because writing correct type hints for our descriptor is... non-trivial.
It would requires several advanced typing features:
@overload, leading to many more lines of code.
(I do have a post covering general type hints for descriptor.)
If you use type hints, you may prefer to stick to the verbose
@property method, as type checkers recognize and verify those types easily.
🦄 Working on a Django project? Check out my book Speed Up Your Django Tests.
One summary email a week, no spam, I pinky promise.
© 2021 All rights reserved.