Class-level Keyword Arguments

by Marty Alchin on January 20, 2011 about Python

Python sports some nifty features when it comes to handling arguments, but those are only for functions. A class declaration is limited to just a list of base classes … or is it?

Supplying one or more base classes in a class declaration looks pretty much like passing positional arguments to a function.

class Example(Base, Mixin):
    pass

You have object references, commas to separate them and parentheses to hold it all in place. I had touched briefly on metaclasses before, but pretty much glazed over the fact that metaclasses can actually receive this list of base classes as a tuple, much like variable positional arguments in a function.

So there’s some precedent here for treating classes and functions somewhat similarly. But the elephant in the room from my earlier post about Biwako is that it’s possible to supply keyword arguments to a class declaration as well!

Metaclasses in Python 3

This is where we get a disclaimer: the technique I’m about to describe is only available in Python 3.0 and higher. Python 3 came with a change to the way metaclasses are specified, using a metaclass keyword argument instead of a __metaclass__ attribute like Python 2 used. So class declarations start to look more like this:

class Example(Base, metaclass=BaseMeta):
    pass

Supporting other arguments

With this change on the table, Python 3 also opens up the possibility for arbitrary keyword arguments. These arguments are provided to the metaclass as part of the class declaration. Both the __new__() and __init__() methods receive them as standard keyword arguments, so you can grab them using the double-asterisk syntax.

class BaseMeta(type):
    def __new__(cls, name, bases, attrs, **options):
        # This is only necessary because type.__new__()
        # doesn't know how to handle the extra arguments
        return type.__new__(cls, name, bases, attrs)

    def __init__(cls, name, bases, attrs, **options):
        print(options)

class Base(metaclass=BaseMeta, option=True):
    pass

Running this code will simply output {'option': True} because Python handles the metaclass as a special case. It’s already figured out which metaclass to use, so it strips that out when sending the rest of the arguments through. What’s left is a dictionary of whatever else you could want your class declarations to accept.

With these new arguments, you can write base classes that can process class-wide options right in the first line of the class declaration itself. Otherwise, you’re stuck using an approach like Django’s, where you have to supply an inner class. It’s functional, and it worked well when it was the only option, but it’s far less elegant by comparison.

Use it wisely

Not all classes can make good use of keyword arguments in this way. For some features, you might be better off using a separate mixin class to define custom behavior. Other situations might make more sense if you simply add attributes to the class directly or special methods that control extra behavior.

There’s no one answer to when you should or shouldn’t use this or any other feature. You always want to research all your options and use the one that makes the most sense, both for now and for maintenance in the future. Hopefully you at least understand this new feature well enough to add it to your toolbox for later.

Stay tuned

My next blog post (probably tomorrow or this weekend) will explain some of the different ways I tried to use class-level arguments in Biwako before finally settling on a solution that just might surprise you.