How to Make Python Statically Typed — The Essential Guide

How to Make Python Statically Typed — The Essential Guide

Type hints, variable annotation, and forcing runtime type checks — everything you need to know.

Python is a dynamically typed language — I’m sure you know that. This makes it easy and fun for beginners, as there’s no need to think about data types. Still, static typing has some benefits. Today we’ll explore how to make Python as statically typed as possible.

So, what’s the deal with dynamically typed languages? In a nutshell, it means there’s no code compilation, so the Python interpreter performs type checking as code runs. As a result, variable types are allowed to change throughout the application.

It may sound like an advantage, but it can lead to strange and hard-to-track errors as the code base gets larger.

On the other hand, statically typed languages perform type checks upon compilation (think C or Java). Further, they give you some feeling of safety, as you can immediately tell the type of parameter going into each function.

Just a quick disclaimer before we start — Python will always be a dynamically typed language. There’s nothing you can do to make it static as Java or C. PEP 484 introduced type hints, so we’ll have to work with that.

The article is structured as follows:

  • Type hints
  • Variable annotations
  • Denoting more advanced data types
  • Forcing type checking on runtime
  • Conclusion

Type hints

Type hints are just that — hints. They show you which data type is expected, but nothing is stopping you from ignoring them. We’ll later explore how to force type checking by the Python interpreter, but let’s cover the basics first.

Let’s make an example without type hints. Below is a function designed to add two numbers and return the sum. Python being Python, we don’t have to specify data types for parameters nor for the return value:

def sum_numbers(a, b):
return a + b
print(sum_numbers(10, 5))
print(sum_numbers(10.3, 5))
print(sum_numbers('Bob', 'Mark'))

The printed results from the function calls are 15, 15.3, and BobMark, respectively. To introduce the type hints, we have to do the following:

  • For parameters — place the colon sign (:) right after the parameter name and specify the data type after
  • For return value — place the arrow sign and the data type right after the parenthesis

Here’s how to add type hints to our sum_numbers function:

def sum_numbers(a: int, b: int) -> int:
    return a + b

print(sum_numbers(10, 5))
print(sum_numbers(10.3, 5))
print(sum_numbers('Bob', 'Mark'))

The function is a bit clearer to look at now, but we can still pass any data types — indicating that hints are just hints, not an obligation.

Here are type hint recommendations, according to PEP 8:

  • Use normal rules for colons, that is, no space before and one space after a colon
  • Use spaces around the = sign when combining an argument annotation with a default value
  • Use spaces around the -> arrow

We’ll see how to force type checks later.


Variable annotations

We can also provide type hints for variables. Once again, these are just hints and do not affect how the code runs.

Here’s a quick example of how to add type hints to variables:

name: str = 'Bob'
age: int = 32
rating: float = 7.9
is_premium: bool = True

Nothing is stopping you from assigning a value of 32.95 to the variable age, or assigning no to is_premium. Hints only indicate that you shouldn’t.


Denoting more advanced data types

Everything we covered thus far is great, but what if you want to declare a list of strings or a dictionary where both keys and values are strings?

That’s where the typing module comes into play. With it, you can use any more complex data types, such as lists, dictionaries, and tuples. You can also declare your own data types, but that’s a bit out of the scope for today.

Let’s now use the typing module to declare a list of names, and then a dictionary of emails, where each email belongs to a single name. In all cases, the data type is a string:

from typing import List, Dict

names: List[str] = ['Bob', 'Mark', 'Jack']
    
emails: Dict[str, str] = {
    'Bob': 'bob@email.com',
    'Mark': 'mark@email.com',
    'Jack': 'jack@email.com'
}

A lot more verbose, but feels safer. Further, let’s see how to make a function parameter to be a list of integers. It’s pretty straightforward:

def list_summation(lst: List[int]) -> int:
    return sum(lst)

You now know how to use type hints to declare a variable of any data type, but how to force type checks on runtime? Let’s explore that next.


Forcing type checking on runtime

I’m not aware of any methods of forcing type checks in the Notebook environment — so we’ll switch to scripts. I’ve made a file called static_typing.py, but feel free to name yours as you wish.

Before proceeding, we’ll have to install a library called mypy. It is used to force type checks on runtime, just what we need. To install it, execute pip install mypy from the Terminal.

We are now good to go. The static_typing.py script contains the following code:

def add_numbers(a: int, b: int) -> int:
    return a + b

print(add_numbers(10, 5))
print(add_numbers(10.3, 5))
print(add_numbers('Bob', 'Mark'))

From the Terminal, execute the script in the traditional way — by writing python static_typing.py

Image 1 — Execution without mypy (image by author)

Image 1 — Execution without mypy (image by author)

The code will run without throwing an error. This is equivalent to what we did earlier in the Notebook. But we don’t want that. With mypy, we can force the type checks. From the Terminal, execute the following: mypy static_typing.py:

Image 2 — Execution with mypy (image by author)

Image 2 — Execution with mypy (image by author)

As you can see, the program crashed. Just the behavior we wanted, as the function expects two integers, but we didn’t provide that data types in the function calls.

And that’s all you should know for starting out. Let’s wrap things up in the next section.


Parting words

Let’s end this article with a couple of pros and cons of using type hints.

Pros:

  • They help to document your code — not a replacement for docstrings though
  • They help in maintaining a cleaner codebase — for obvious reasons
  • They improve the linting capabilities of your code editor — for example, PyCharm will show a warning if a type mismatch is detected

Cons:

  • Can be tedious to add — especially if you are not accustomed to them

To conclude — type hints are not required but are good practice, especially when working on large codebases — even if you decide not to use mypy.

What are your thoughts on type hints? Do you always use them, or just when the codebase gets larger? Let me know in the comment section below.