How to Create a Transparent Attribute Alias in Python

2021-10-13 “You don’t know my secret alias,” said the hedgehog.

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.

Using @property

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

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 [1]: widget = Widget(1337)

In [2]: widget.cycles
Out[2]: 1337

In [3]: widget.cycles = 9001

In [4]: widget.cycles
Out[4]: 9001

In [5]: widget.rotations
Out[5]: 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 del statement.)

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.

Enter the descriptor

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:

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 @classmethod, @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. Our __get__ and __set__ methods then proxy getting/setting the underlying attribute on the instance (obj). __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 __delete__ method.)

We can use our descriptor like so:

class Widget:
    def __init__(self, rotations: int) -> None:
        self.rotations = rotations

    cycles = Alias("rotations")
    turns = Alias("rotations")

Each of cycles and 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 [1]: widget = Widget(1024)

In [2]: widget.turns
Out[2]: 1024

In [3]: widget.cycles
Out[3]: 1024

In [4]: widget.turns = 2048

In [5]: widget.rotations
Out[5]: 2048

In [6]: widget.cycles
Out[6]: 2048

In [7]: widget.turns
Out[7]: 2048

Seems legit!

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 Alias class. This is because writing correct type hints for our descriptor is... non-trivial. It would requires several advanced typing features: Generic, TypeVar, and @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.

Fin

May your aliases serve you well,

—Adam


🦄 Working on a Django project? Check out my book Speed Up Your Django Tests.


Subscribe via RSS, Twitter, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: python