Table of contents
Python classes are great. But they can be better.
Take a look at this class, let's call it Foo:
class Foo: pass >>> Foo > 0 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: '>' not supported between instances of 'type' and 'int'
Yes, try as it might, this puny class doesn't even know if it's bigger than 0 or not!
It would not help to define a __gt__
method for it, because those only run for instances, not on classes.
This regular class, class Foo, is forever doomed to not knowing how big it is. Never unlocking its true potential.
Now look at my class. It is an uberclass, and it's bigger than anything you can ever conjure:
@uberclass class Biggest: def __class__gt__(cls, other): return True >>> Biggest > 1 # Of course True >>> Biggest > Foo # Glorious! True
Observe its unquestionable gigantic-ness. Observe its grotesque elegance. But does it really work?
Of course, you don't yet know what is an uberclass, because I just invented it.
Nor still are you privy to its dark machinations, and the dark and forbidden magic coursing through its veins, commonly known as the metaclass.
But yes, it most definitely works, and by the end of this blog post, you'll also know how.
Enter the metaclass
A lot has been written about metaclasses, and most of it is unintelligible. Suffice to say that metaclasses allow you to define methods for the class itself, instead of for its instance.
And so, you might already see how one might write a Biggest class using them. Here is how regular Python wizards would do it:
class BiggestMetaclass(type): def __gt__(cls, other): return True class Biggest(metaclass=BiggestMetaclass): pass >>> Biggest > "whatever" True
Yes, it works. This is already leaps and bounds beyond what mere mortals can accomplish in Python. But it's not enough. It's not enough!!!
It's clumsy. It's confusing. You have to define a separate class for it. You have to use the metaclass=
feature for it, a simple decorator won't do. And yet, it could.
So buckle up. We're going to take it to the next level.
Enter the Uberclass
The Uberclass is a class that supports defining methods for both the instance and the class, within the same namespace.
It's easy to use, and easy to compose. All it takes is just a single function call, or better yet, a single class decorator.
It is Yin and Yang. Dark and Light. Order and Chaos.
:: apply_metaclass()
Before we can create the Uberclass itself, first with have to define a dark helper - apply_metaclass()
- which binds an existing class to a new metaclass, by using a class wrapper:
def apply_metaclass(cls, methods, metaclass_name='apply_metaclass'): "Creates a subclass of 'cls' with the given methods in its metaclass" # Create metaclass (override repr to hide wrapping) def meta_repr(self): return repr(cls) metaclass = type(metaclass_name, (type,), {'__repr__': meta_repr, **methods}) # Create wrapper with metaclass class _Wrapper(cls, metaclass=metaclass): pass _Wrapper.__name__ = cls.__name__ return _Wrapper
This function is almost trivial. We override the __repr__
and __name__
attributes of the wrapper, in order to conceal its wretched origins. It should be almost indistinguishable from cls
. Finally, the metaclass is given the methods
argument, or should I say "meta-methods", so we can change the behavior of the resulting class.
Here's an example of how you might use it:
>>> my_float = apply_metaclass(float, {'__repr__': lambda n: f'FloatyFloaterman'}) >>> my_float FloatyFloaterman >>> my_float(3.14) 3.14 >>> type(my_float(3.14)) FloatyFloaterman
(Our repr overrides the default one)
Ah, what a sweet, beautiful evil we have created. Had this been the sum of our accomplishments, it would already be enough to sow decay and chaos within the unsuspecting Python community.
But we are going to take it one step further. We are going to summon the Uberclass.
:: uberclass()
By using our helper function, it ends up being quite simple:
def uberclass(cls: type, prefix: str = '__class'): "A class decorator that creates an Uberclass" # Filter and rename methods based on prefix d = {k[len(prefix):]: v for k, v in cls.__dict__.items() if k.startswith(prefix)} # Return cls wrapped with a new metaclass return apply_metaclass(cls, d, cls.__name__)
First, we take all the class-methods (so to speak) from the __dict__
attribute, and rename them so that __class__gt__
becomes __gt__
. Otherwise, there will be a collision between the names of the class methods and the instance methods.
Then, we call our helper function with the renamed class methods. Simple as that.
Let's test it out:
@uberclass class MyUberClass: def __class__instancecheck__(cls, other): return other == "UberInstance" def __class__add__(cls, other): return "class_add" def __add__(self, other): return "instance_add" >>> isinstance("UberInstance", MyUberClass) True >>> MyUberClass + 10 class_add >>> MyUberClass() + 10 instance_add
We have created a thing of beauty. Let us rejoice. Let us weep.
But what good is evil, if we can't unleash it unto the world? Let's move on to a practical use-case you might actually be tempted to use in real-life.
Practical use-case
In Python 3.10, they introducted a new type-union operator. Essentially, it lets you use the pipe operator to union Python types. So, you can write something like int | str
instead of Union[int, str]
, which is a lot shorter and doesn't require an import.
It's really too bad that it doesn't work for any earlier version of Python. Or... can it?
from typing import Union @uberclass class int(int): def __class__or__(self, other): return Union[self, other] >>> issubclass(str, int | str) True >>> issubclass(int, int | str) True >>> issubclass(float, int | str) False
Yes, we just wrote a backport for pep-604!
Conclusions
- Metaclasses are confusing
- Uberclasses are superduper
- I might be watching too much TV
If you liked this post, you might be interested in my runtime type library for Python, called Runtype. It doesn't have uberclasses yet. But, perhaps, it is only a matter of time.
Appendix: Full code for uberclass
Here is the full code for defining an uberclass decorator. Use it at your peril:
def apply_metaclass(cls, methods, metaclass_name='apply_metaclass'): "Creates a subclass of 'cls' with the given methods in its metaclass" # Create metaclass (override repr to hide wrapping) def meta_repr(self): return repr(cls) metaclass = type(metaclass_name, (type,), {'__repr__': meta_repr, **methods}) # Create wrapper with metaclass class _Wrapper(cls, metaclass=metaclass): pass _Wrapper.__name__ = cls.__name__ return _Wrapper def uberclass(cls: type, prefix: str = '__class'): "A class decorator that creates an Uberclass" # Filter and rename methods based on prefix d = {k[len(prefix):]: v for k, v in cls.__dict__.items() if k.startswith(prefix)} # Return cls wrapped with a new metaclass return apply_metaclass(cls, d, cls.__name__)