Don’t Forget That Python Is Dynamic! Marcin Kozak


This whole situation begs the following question: Do we indeed need all those type hints, static type checkers, and runtime type checkers?

I am not going to respond to this question. This is mainly because I am far from being one of those people who think they know everything about… well, about everything, or almost everything. But I hope to invite you to think about this yourself.

I will, however, remind you — and myself — that Python’s dynamic typing, also called duck typing, lies behind the success of this language. Below is the popular explanation of how duck typing works:

If it walks like a duck and it quacks like a duck, then it must be a duck.

Duck typing can be very powerful without type hints and runtime type checking. I will show you this on very simple examples, and without further ado, let’s jump into this simple example:

Catching errors by runtime type checking, and customizing message
Catching errors by runtime type checking. Image by author

Here, we’re checking types of x and y, and both should be strings (str). Note that this way, we’re sort of checking whether what we provide to the str.join() method is tuple[str, str]. Certainly, we don’t have to check if this method gets a tuple, since we’re creating it ourselves; enough to check the types of x and y. When either of them is not a string, the function will raise TypeError with a simple message: "Provide a string!".

Great, isn’t it? We’re safe that the function will be run only on values of correct types. If not, we will see a customized message. We could also use a custom error:

Now, let’s remove the type check and see how the function works:

Catching errors without runtime type checking; the message is not custom by built-in
Catching errors without runtime type checking; the message is not custom by built-in. Image by author

Ha. It seems to be working in quite a similar way… I mean, an exception is raised basically in the same place, so we’re not risking anything. So…

Indeed, here the function foo_no_check() uses duck typing, which uses a concept of implicit types. In this very example, the str.join() method assumes it takes a tuple of strings, so both x and y have to be strings, and if they aren’t, the implicit type for tuple[str, str] has not been implemented. Hence the error.

You could say: “But hey! Look at the message! Before, we could use a custom message, and now we can’t!”

Can’t we indeed? Look:

Catching errors without runtime type checking. The error message is customized via a try-except block.
Catching errors without runtime type checking; the error message is customized. Image by author

We can now see both messages: the built-in (sequence item 1: expected str instance, in found) and custom (Provide string!).

You could ask: What’s the difference? So, I check types. What’s the problem?

Well, there is quite a difference: performance. Let’s benchmark the three versions of the function, using the perftester package:

Here are the benchmarks:

perftester benchmarks
Benchmarks performed using the perftester package. Image by author

For all benchmarks in this article, I used Python 3.11 on a Windows 10 machine, in WSL 1, 32GM of RAM and four physical (eight logical) cores.

In the second line, I set the default number of experiments to 10, and inside each run, each function it to be run a hundred million times. We take the best out of the ten runs, and report the mean time in seconds.

The foo() function, so the one with the runtime type checks, is significantly slower than the other two. The foo_no_check() function is the fastest, although foo_no_check_tryexcept() is only a little slower.

Conclusion? Runtime type checks are expensive.

You could say: “What? Are you kidding me? Expensive? It’s just a minor part of a second! Not even a microsecond!”

Indeed. It’s not much. But this is a very simple function with only two checks. Now imagine a big code base, with many classes, methods and functions — and a looooot of runtime type checks. Sometimes, this may mean a significant decrease in performance.

Runtime type checks are expensive.

When reading about duck typing, you will usually see examples with cats that meow, and dogs that don’t, and cows that moo. When you hear an animal meowing, it’s neither a dog nor a cow, it’s a cat. But not a tiger. I decided to use an atypical example, and I hope it was clear enough for you to see the strengths of duck typing.

As you see, Python exception handling does a great job in runtime type checking. You can help it by adding additional type checks when needed, but always remember that they will add some overhead time.

Conclusion? Python has great exception-handling tools that work quite well. Oftentimes, we do not have to use runtime type checking at all. Sometimes, however, we may need to. When two types have similar interfaces, duck typing can fail.

For instance, imagine you want to add two numbers (x + y), but the user provided two strings. This will not mean an error because you can add two strings. In such instances, you may need to add a runtime type check, if you don’t want the program to continue with these incorrect values. Sooner or later it can break anyway, so the question is whether you want the program to continue until then or not. If yes, you may risk an exception will be raised much later, so adding a type check can actually help save time. In addition, raising the exception later in the program flow can mean difficulties in finding a true reason behind the error.

All in all, I do not want to tell you that runtime type checking should never be used. But oftentimes, such checks are added when they are not needed.



Source link

Leave a Comment