The world’s leading publication for data science, AI, and ML professionals.

Don’t Forget That Python Is Dynamic!

More and more static and dynamic checks… Is it where we want Python to head?

PYTHON PROGRAMMING

Python is a dynamic language. Over recent years, however, more and more focus has been paid to static type checking. This, in turn, leads to an increased interest in runtime type checking. How far will we go with that? In this article, we will recall why Python was, not that long ago, considered a powerful dynamically typed programming language.

Is it still?


Python’s strength has always lied in its simplicity, which, at least partially, resulted from dynamic typing, not only because we can write Python code in REPL, but also for the following reasons:

  • You can easily change a variable’s type throughout a program.
  • You do not have to define the type of a variable.
  • The code is (or can be) easy to read and understand.
  • Sometimes, you can use a couple of lines of code in order to implement even quite a complex algorithm. A statically typed language usually needs much more – or at least more – lines of code.

Sure, dynamic typing does not come without cost. The first one is decreased performance, something we all know. This decrease comes from the fact that types – which aren’t declared – have to be checked at runtime (it’s done by Python). Another cost is the increased risk of runtime errors: since types are not checked at compile time but at runtime, related errors are thrown during program execution instead of during compiling.

We need to remember that Python offers a set of tools to handle its decreased performance:

The Speed of Python: It Ain’t That Bad!

For years, Pythonistas were both proud and happy that Python was a dynamically typed language. Sure, those who did not like Python claimed it was a bad language… What can I say? Everyone can think whatever they want to; programming is a free universe.

Programming is a free universe.

For some time, however, Python has been moving towards the static side of programming. The most important aspect of this movement is Type Hints. Sure, they are optional, but most big projects written these days do implement type hints. You will more often hear that in a serious project you must hint types than that you don’t have to – not to mention that you shouldn’t. Be prepared to hear something like this: "Sure, type hints are optional, so for prototypes and short scripts you don’t need to use them, but for big projects – well, there is no other option than to hint types." I’ve heard it not once and not twice.


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:

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:

Should we use custom exceptions in Python?

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

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:

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

Performance

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](https://github.com/nyggus/perftester) package:

Benchmarking Python Functions the Easy Way: perftester

Here are the benchmarks:

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.

Conclusion

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.


I hope I intrigued and inspired you. Today, this is all I wanted to achieve. Please share your thoughts in the comments. Tell us if you would or would not use runtime type checks in such simple situations.

I haven’t discuss static checking and goose typing. I have written several articles static checking and type hints:

Python’s Type Hinting: Friend, Foe, or Just a Headache?

Python Type Hinting: Duck Type Compatibility and Consistent-With

Python Type Hinting: From Type Aliases To Type Variables and New Types

I haven’t yet written about goose typing, but sooner or later, the time will come.


Is Python still considered a powerful dynamically typed language? To be honest, I don’t know. So much focus in Python is on static and runtime checking that I’m afraid many have forgotten that the true power of Python lied in completely different things: its simplicity, readability – and yes, duck typing.

I have heard, however, experienced Pythonistas expressing their sadness that Python is not what it was not that long ago. Some of them decided to move from Python to other languages, claiming that "If I want to use a language in which I need to define types for variables, Python would be my last choice!" It makes plenty of sense. Statically typed languages can be much faster than Python – and still, they can be quite simple and readable, like Go. But Python… Python offers simple and powerful duck typing… Duck typing that many seem to forget.

I myself love both Python and Go. Go is statically typed, and it’s one of the things that make it visibly faster than Python. But hey, you know what? Go’s statically typed code is, for me, often much easier to read and understand than Python’s code with type hints!

When I see runtime checks all over the code, I feel tired and discouraged. It’s not what Python was created for. Yes, type hints can be quite helpful – but only when used right and when not overused. If overused, they can become quite a burden.

When I see runtime checks all over the code, I feel tired and discouraged. It’s not what Python was created for.


Thanks for reading. If you enjoyed this article, you may also enjoy other articles I wrote; you will see them here. And if you want to join Medium, please use my referral link below:

Join Medium with my referral link – Marcin Kozak


Related Articles