Python Beginner

Static Typing in Python

Conducting Type Checking with Ease

Eden Au
Towards Data Science
5 min readApr 4, 2020

--

Python is a dynamically typed language. That means it is not necessary to declare the type of a variable when assigning a value to it. For instance, you do not need to declare the data type of object major as string when you initialise the object with a string value of 'Tom'.

major = 'Tom'

In a statically typed language like C, you must declare the data type of an object. A string is declared as an array of characters.

char major[] = "Tom";

Coding in a dynamically typed language like Python surely is more flexible, but one might want to annotate data types of objects and enforce type constraints. If a function only expects integer arguments, throwing strings into the function might crash the program.

While this is one of the major pitfalls of dynamic typing, Python 3 introduces several annotation tools for programmers to specify and constraint data types of objects.

Function Annotations

Let’s take a very simple function foo as an example:

def foo(n, s='Tom'):
return s*n

The function takes n and s as arguments, and returns s*n. While it might look like a straightforward and pointless multiplication function, notice the default value of s is 'Tom' which is a string, not a number. We might infer that this function intends to return a string that repeats the string s multiple times — n times to be exact.

foo(3, 'Tom') # returns 'TomTomTom'

This function is quite confusing. You might be tempted to write lengthy comments and docstrings that explain the function and specify the data types of arguments and return values.

def foo(n, s='Tom'):
"""Repeat a string multiple times.
Args:
n (int): number of times
s (string): target string
Returns:
(string): target string repeated n times.
"""
return s*n
Photo by NASA on Unsplash

Python provides a more compact way for you to do optional annotation using symbols : and ->.

def foo(n: int, s: str='Tom') -> str:
return s*n

The annotations of the function foo is available from the function’s __annotations__ attribute. It is a dictionary that maps parameter names to their annotated expressions. This allows manual type checking by running the code instead of looking into the source code by yourself. Pretty handy.

foo.__annotations__
# {'n': int, 's': str, 'return': str}

Variable Annotations

Apart from function arguments and return values, you can also annotate variables with a certain data type. You can also annotate variables without initialising them with any values!

major: str='Tom' # type:str, this comment is no longer necessary
i: int

It is better to annotate variables using this built-in syntax instead of comments, as comments are often greyed out in many editors.

For annotating variables with more advanced types such as list, dict etc., you would need to import them from module typing. The name of the types are capitalised, such as List, Tuple, Dict etc..

from typing import List, Tuple, Dictl: List[int] = [1, 2, 3]
t1: Tuple[float, str, int] = (1.0, 'two', 3)
t2: Tuple[int, ...] = (1, 2.0, 'three')
d: Dict[str, int] = {'uno': 1, 'dos': 2, 'tres': 3}

The elements inside a list, tuple, or a dictionary can also be annotated. Those capitalised types take parameters in square brackets [] as shown above.

List takes one parameter, which is the type annotated for all elements inside the list. Elements in a fixed size tuple can be annotated one by one, whereas those in a variable size tuple can be annotated by ellipsis .... We can also specify types of keys and items in a dictionary as well.

Photo by Fabien Bazanegue on Unsplash

Advanced Annotations

We mentioned that List only takes one parameter. What about annotating a list that contains a mixture of int and float elements? Union is the answer.

from typing import Union
l2: List[Union[int, float]] = [1, 2.0, 3]

It also supports any user-defined classes as types in annotations.

class FancyContainer:
def __init__(self):
self.answer = 42
fc: FancyContainer = FancyContainer()

A callable can also be annotated using the above-mentioned techniques. A callable is something that can be called, like a function.

from typing import Callable
my_func: Callable[[int, str], str] = foo

Caveats

First, type annotations do not entirely replace docstrings and comments. A brief description and explanation of your functions are still needed for readability and reproducibility purposes. Enabling type annotations can avoid having convoluted comments filled with information like data types.

Second, there is something that I should have told you from the beginning. Python interpreter actually does not automatically conduct any type checking whatsoever. That means those annotations have no effects during runtime, even if you are trying to pass a ‘wrong’ type of objects to a function.

So have you wasted your own time trying to learn type annotations? No.

There are many Python modules available that enforce those constraints before runtime. mypy is by far the most commonly used type checker with no runtime overhead.

Photo by REVOLT on Unsplash

The Takeaway

Type annotations in Python are helpful for debugging and optional type checking that mimics static typing. It becomes increasingly popular in project development, but still very uncommon across more casual Python programmers.

While the absence of annotations does not necessarily degrade code performance, it is still considered as a good practice for robustness, readability, and reproducibility purposes.

We have barely scratched the surface of static typing and type annotations in Python. Python 3.9 is coming soon with some upgrades on variable annotations, so stay tuned by signing up for my newsletter to receive updates on my new articles.

Not even ready for Python 3.8? I have you covered.

Thanks for reading! Do you find these features interesting and useful? Leave a comment below! You might also find the following articles useful:

--

--