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.
Just not caring
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.
|
|
Inheritance
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.
|
|
Interfaces
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.
The Duck Test
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 quack()
and walk()
is a duck. That’s easy enough to check.
|
|
This works. But we list the isinstance(...)
call. We can surely do better.
Metaclasses & Subclass Hooks
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 isinstance(...)
, using __subclasshook__
.
|
|
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?
Abstract Methods, Again!
Lucky for us - we can!
Among other things, the ABC
parent class enumerates all @abstractmethod
s 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.
Protocols
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.
This Dog is a Duck
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.
Summary
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.
Further Reading
The metaclass techniques demonstrated here are simplified versions of code from the abc
and 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.