Duck typing is great. Knowing that as long as my object does what the function expects it to, I can pass it to the function and get my results without having to worry about exactly what else my object might do. Coming from statically-typed languages such as Java and C++, this is incredibly liberating, and makes it easy to rapidly prototype complex and expressive code without worrying about checking types everywhere. This expressiveness, however, comes with a cost: type errors are only caught at runtime, and can be hard to debug if the original author didn’t document what that one variable in that one function signature is expected to look like.
For example, let’s define two birds, both of which can fly, but only one of which honks:
class Duck: def fly(self): print("Ducks can fly") def quack(self): print("Quackers!") class Goose: def fly(self): print("Geese can fly") def honk(self): print("Honk!")
Now let’s add a function to make them migrate:
def migrate(bird): bird.fly()
Notice how I don’t have to tell migrate() what type of object to expect, and at no point am I required to assert that bird is, in fact, a bird. As long as bird can fly, it can migrate without any problems. The code will run as expected, we get our results, and the bird survives another winter. Indeed, anything that can fly can migrate, though the original author likely didn’t anticipate their code being used to fly Eurofighter Typhoons south for winter.
Suppose, now, we want to make sure Bruce the Goose announces his arrival for the holidays. We might update the migrate() function as follows:
def migrate(bird): bird.fly() bird.honk()
As long as we pass a Goose (or anything else that can fly and honk) to migrate, everything is fine. If however we want our ducks to migrate, we’re met with the following runtime error:
AttributeError: ‘Duck' object has no attribute 'honk'
Not very helpful. We know ‘honk’ is a function, not an attribute, but this error doesn’t tell us this. It’s an error you get used to debugging after working with python packages for a while, but back when I was a noob this sort of problem caused me endless headaches. Worse, even if I used an IDE like PyCharm to inspect and debug my code, there is no guarantee that it would be able to figure out that migrate() needs a bird that can honk. The metadata simply isn’t there.
This is where type annotations can make our lives, and the lives of the unfortunates upon which we inflict our code, much easier. Introduced in Python 3.5 (yet another reason to retire your fossilized 2.x distribution) and described in PEP484, type hinting provides a syntax to annotate function arguments and returns with a type or set of types in order to inform the user (or their IDE) exactly what the function can be expected to handle, and what they can expect to get back out. This was later extended in PEP526 to provide a syntax for annotating variable declarations with types. Type hinting is entirely optional, and does not affect the execution of the code at all. If your IDE tells you a function expects a numpy array, but you know a list will work for your use case, your code won’t break at runtime just because you fought the system.
So if it doesn’t stop the dreaded runtime errors, and doesn’t even give us something more useful than ‘AttributeError’, why should we care about type hinting? Remember our poor ducks from earlier, and how PyCharm couldn’t tell me they need to honk to make it home for Christmas? If I’d annotated the migrate() function to tell the user it only works for geese, rather than any old bird, PyCharm would immediately recognize that I was trying to pass a Duck in place of a Goose and would warn me about a potential type mismatch. Here’s what the annotated function looks like:
def migrate(bird: Goose) -> None: bird.fly() bird.honk()
Now, if we try to send the ducks south, we’ll still get an AttributeError, but at least PyCharm can tell us what the problem is, saving us the pain of figuring out exactly what the functional difference between a Duck and Goose is. It’ll even tell you the function returns None, so you don’t accidentally try to use it for coconut delivery.