Static Typing in Python
Dynamically typed languages such as Python are notorious for being error-prone. Here are a few techniques for overcoming a type of error which would not occur in statically typed languages.
Dynamic Typing vs Static Typing
When defining a function in a statically typed language, such as C or Rust, one must always declare the function argument types. This practice, while seemingly restrictive, actually empowers developers. By defining types themselves, usually using a struct call, developers can catch potential errors at compile time. The compiler will check whether all function calls respect the defined parameter type. If not, the compiler will refuse to compile. This means, for example, that when one establishes a function:
|
|
On the other hand, Dynamically typed languages do not require defining the type of a parameter. For example, in Python, you could write the following:
|
|
You see that the results were different, but the function did run without an
Error. This is only sometimes the case. For example, if Message
had another
property defined and then we tried to access it, it would raise an exception
(AttributeError
). This trivial example is obvious, but when you try to figure
out why a function is not acting as you would think, it becomes extremely
annoying.
Benefit Dynamic Typing
The first obvious benefit of dynamic typing is that the code is much less verbose because you don’t need to explicitly define the types. However, the programmers are encouraged to use type hints, so the resulting verbosity of the code is similar to the statically typed language.
Another benefit of dynamic typing is the ease of passing collection types (arrays, lists, and especially dictionaries), which store different types, to a function. This is doable in statically typed languages but requires more effort on the developer’s part.
Drawbacks of Dynamic Typing
In statically typed languages, errors due to wrong types are caught during compilation, ensuring a robust and predictable system. This is in contrast to dynamically typed languages, where such errors can lead to unpredictable function outputs.
Furthermore, the type hints are encouraged but not enforced. Any code where the type of an argument is not immediately prominent is inherently less readable. Imagine deducting a variable in a method that calls for some attribute defined in some external library, which was not even imported.
For more comparisons of the two, visit the following links links: stackoverflow_1, stackoverflow_2.
Adding types to the Python
Suppose you use Python on a regular basis but want to overcome the issues
created by the dynamically typed nature of it. In that case, you can try a few
approaches to mitigate the possible type errors. Before I go into the details, I
want to mention our sponsor, ~ type Hinting~. With just a few keyboard strokes,
you can let future you and the language server protocol know what types you
should pass into the function or method. Type Hinting
seamlessly integrates with
a built-in library typing
, which you can use to define your types precisely:
|
|
Runtime type checking
When you have your types defined, you can start to check the types you are passing to a specific function. You can do it during runtime, leading to some computational overhead. Some solutions can help with type-checking.
Pydantic
Pydantic is an external library primarily used for validation. Its core logic is
implemented in Rust, leading to fast data validation. Generally, pydantic is
used for serialization in networked applications, but it also contains a neat
decorator @validate_call
:
|
|
Pydantic allows for validation of complex and nested types. The logic is quite complex; thus, a less complex solution can be used in simple cases.
Custom Type Enforcing
Pydantic can be too heavy in some situations for the specific use case. In those situations, you can write a custom type enforcing decorator
|
|
To avoid defining your own decorator, you can also use type-enforced
library. Simply install it with pip
import it and decorate:
|
|
Static Type Analysis
You can use mypy
for static type checking to avoid runtime type checking. You
use it similarly to the testing suite, entailing running the mypy
process once
before you use the code. This means that you can include it in your staging
workflow. To use mypy, you first need to install it with pip.
|
|
To perform the analysis, run the mypy
command on the file you want to check.
|
|
Automatic Type Annotations
Sometimes, someone has written a lot of code without type hints, and you want an
easy way of assigning the types. In this case, you can use a tool named
MonkeyType. To generate the type, you can then use the monkeytype
command on
your source code.
For example if you have a module greeting.py
|
|
and then in another module, say main.py
, you import it and use the function:
|
|
Now you can execute:
|
|
After run is completed, you can inspect the types:
|
|
Which will generate the output:
|
|
As you can see it picked up also on the integer type, which we used in the
main.py
. Let’s leave it like that and apply the type hints:
|
|
Now our source code of greetings.py
was updated with the type hints:
|
|
Because of the fact that this changes your source code, it is advised that you
use git
to be able to reverse if incorrect type hints are applied.
Conclusions
We have covered techniques that make Python more resilient to type-related errors. With this approach, you can avoid these errors and make debugging experience more manageable.