After multiple attempts at finding a funny narrative that holds for the entire article and failing miserably, I decided to go with the technical parts alone. Enough of my colleagues found it interesting, so I guess it will hold up without the jokes.
Python gives us multiple ways to check that the objects we pass to functions are of the types we expect. Each method has its advantages and disadvantages.
The first option is naturally to not care about types - just write your code, and hope for the best. This is a viable method, and is often employed. It is especially fitting in short snippets or scripts you don’t expect to maintain much. It just works with no overhead whatsoever.
Another option, as common in OOP languages, is to use inheritance. We can define an
Anas class, and expect all of its derivatives to be sufficiently duck-like.
While inheritance kinda gets the job done, a robotic duck is definitely not of the genus Anas, while it does implement all the characteristics we care about. So instead of hierarchical inheritance, we can use interfaces.
Great. And if we don’t control the types, we can always write adapters.
But this is Python. We can do better.
As we know, Python uses duck-typing. So we should be able to use the Duck Test for types. In our example, every object implementing
walk() is a duck. That’s easy enough to check.
This works. But we list the
isinstance(...) call. We can surely do better.
Metaclasses are wonderful constructs. As their name may suggest, they take part in the construction of classes. They even allow us to set hooks into basic Python mechanisms, like
And we’re back in business. That said,
is_a_duck is still a stringly-typed mess, and gonna be very painful to maintain.
Wouldn’t it be nice if we could just use our
IDuck interface to check for duck-ness?
Lucky for us - we can!
Among other things, the
ABC parent class enumerates all
@abstractmethods and stores them in the
__abstractmethods__ member variable. This means that we can easily enumerate them in our subclass hook and check for them.
Awesome. Next step - separating the interface from the checking logic.
Reading through Python documentation and nomenclature, you might have seen the term “protocol” here and there. It is Python’s way to call duck-typed interfaces. So you could say we just created a “protocol checker”. Now, we can separate it into a base-class.
And that’s it. That little
_is_protocol flag is there for good reason. Usually, we’d check protocol-ness using
isinstance(...). In this case, however, we’re hooking into that mechanism and that would lead to infinite recursion.
We can now use our
Protocol base-class freely to create new protocols as we need them, with friendly interface-like syntax. We’re almost done.
In some cases, the protocol checks may not be what we want. The obvious reasons coming to mind are:
- We can’t really check the desired semantics using the protocol trick.
- We want to wreck havoc.
For those cases (well, mostly for the first one) the
ABC base class provides another trick. Instead of defining
__subclasshook__ to check the interface, we can simple register classes as valid, “virtual subclasses”.
Remember that this method puts all the pressure on the programmer. Writing
IDuck.register(Dog) is the equivalent of saying “I vouch for this dog to be a duck”. It might pass inspection, but won’t necessarily yield the desired results.
In this article we covered multiple ways of checking the “duck-ness” of objects. From belonging to the Anas genus, to just placing a sticker on their head saying “Duck!”. Some of those methods are more useful or applicable than others, but I still think it worthwhile to be familiar with all of them. Additionally, there are many topics not covered here, like static type checking.
The metaclass techniques demonstrated here are simplified versions of code from the
typing modules. I highly recommend going through those modules and their respective docs, at least at a glance, to extend your knowledge and cover up any holes left by my hand-wavy explanation.