Python's Static Typing Safari: In Search of Code Clarity
Growing a one-line addition function into annotations, overloads and TypeVar generics, so type mismatches surface in the editor instead of at runtime.
Introduction
In the vast landscape of programming languages, Python stands out as a dynamically typed language, celebrated for its readability and simplicity. Its syntax, clear and concise, makes it an ideal choice for scripting and automating processes with just a few lines of code. However, as projects grow in complexity and involve multiple contributors, the need for a more robust syntax becomes evident.
In this exploration, we dive into the realm of Python’s static typing, unraveling its nuances and unveiling the tools at our disposal. Beyond the simplicity of dynamically typed languages, Python provides features like type annotations, function overloading, and generics, offering developers a sophisticated toolkit for crafting elegant and maintainable code.
Let’s embark on a journey through the evolution of a simple addition function, uncovering the power of type annotations, the resurgence of overloading, and the elegance introduced by generics in Python.
Bug alert!
Consider the following lines of code:
def mysum(a, b): return a + bThis function sums two elements. In this case, it might seem reasonable to leave it as it is, as you get function overloading for free on types:
mysum(1, 2) # a: int, b: int -> intmysum(0.5, 1.2) # a: float, b: float -> floatmysum(1, 0.6) # a: int, b: float -> floatmysum(7, 4+2.3j) # a: int, b: complex -> complexAll the calls to the mysum function are correct, and each time Python is able to infer the correct
return type. Similarly, these function calls are valid and correct:
mysum([1, 2, 3], [4, 5, 6])mysum("foo", "bar")However, what happens when you start calling the function with mixed arguments?
mysum(1, "foo")mysum({"a": 2}, [1, 4, 7])mysum(5, None)These lines are legitimate Python, yet most text editors won’t complain. However, once the script executes, the Python interpreter crashes against these type mismatches:
Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in mysumTypeError: unsupported operand type(s) for +: 'int' and 'NoneType'Type annotations
TypeError! How is this possible? Who dared to tell the interpreter to check types? Even though the use
of types in Python is mostly transparent, it doesn’t mean they don’t exist. Therefore, annotating types
in Python can be a good idea to save hours of debugging. Consider the following:
def mysum_number( a: int | float | complex, b: int | float | complex,) -> int | float | complex: return a + b
def mysum_list(a: list, b: list) -> list: return a + b
def mysum_str(a: str, b: str) -> str: return a + bLet’s break down the code above. The first function, mysum_number, accepts two arguments of type
int, float, or complex and returns a value of the same type. The second function, mysum_list,
accepts two arguments of type list and returns a value of the same type. The third function,
mysum_str, accepts two arguments of type str and returns a value of the same type.
Now, every time you want to add two elements, knowing their type is enough to decide which function to
call. However, this precision sacrifices overloading. To call all functions in the same way, indicating
the desire for an overload, the standard library typing contains the overload decorator. The
refactored code looks like this:
from typing import overload
@overloaddef mysum( a: int | float | complex, b: int | float | complex) -> int | float | complex: ...
@overloaddef mysum(a: list, b: list) -> list: ...
@overloaddef mysum(a: str, b: str) -> str: ...
def mysum(a, b): return a + bOverloading is back. Now, the function calls seen earlier all work, and those that used to generate a
TypeError at runtime are now highlighted in red by our IDE.
Generics
We could consider ourselves satisfied like this, but the typing module offers many other features.
Among these, the use of Generics stands out, which in this case can be exploited to create a more
elegant solution:
from typing import TypeVar
T = TypeVar("T", int, float, complex, str, list)
def mysum(a: T, b: T) -> T: return a + bTypeVar allows you to define a type that varies depending on the context, but once it is determined,
it cannot change:
mysum(1, 2) # T = intmysum(5, 0.7) # T = floatmysum("a", "b") # T = strSince Python 3.12 the T typevar declaration can be omitted1, and the type can be directly specified
in the function definition:
def mysum[T: (int, float, complex, str, list)](a: T, b: T) -> T: return a + bSum up
In this exploration of Python’s static typing, we delved into crucial concepts that enhance code clarity and maintainability. Let’s recap the key takeaways:
-
Clear syntax for all sizes: Python’s dynamic typing shines in small scripts, offering a clear and concise syntax. However, as projects grow, the need for a more expressive syntax becomes apparent.
-
Navigating overloading: The journey from a dynamically typed function to an overloaded one, thanks to the
typingmodule’soverloaddecorator, showcases Python’s adaptability to diverse project requirements. -
Type annotations as guides: Embracing type annotations is akin to providing a map for your code. Explicitly specifying types not only aids in debugging but also serves as documentation, making your code more understandable and maintainable.
-
Generics, the elegance factor: The introduction of generics via
TypeVarelevates Python’s static typing to new heights. With constraints on acceptable types, developers can craft more elegant and type-safe solutions. -
Evolution of Python’s typing tools: From annotations to overloading and generics, Python’s typing tools evolve to cater to various development scenarios, offering a versatile toolkit for developers.
As you navigate your coding endeavours, remember that Python’s static typing is not about rigid constraints but about providing developers with powerful tools to enhance their code’s expressiveness and robustness. Embrace these tools judiciously, and may your coding journey be both clear and elegant.
Footnotes
cite this post
Spanio, M. (2023, November 10). Python's Static Typing Safari: In Search of Code Clarity. Matteo Spanio. https://matteospanio.github.io/blog/2023/python-static-typing/
@misc{spanio2023pythonsstatictypin,
author = {Spanio, Matteo},
title = {Python's Static Typing Safari: In Search of Code Clarity},
year = {2023},
month = nov,
howpublished = {Blog post, \url{https://matteospanio.github.io/blog/2023/python-static-typing/}},
url = {https://matteospanio.github.io/blog/2023/python-static-typing/}
}