Intro to Python Type Hinting

Posted by Ryan Himmelwright on Sun, Jan 30, 2022
Tags dev, python, programming
Surf City, NC

I have been trying to code daily the last several weeks. Shortly after starting a project in Python, I decided to refactor it to use Python type hinting. Type hinting is a newer feature of the language that I’ve known about, but hadn’t yet used much. So, I gave it a shot.

Why?

Before getting started, you might be asking yourself “Why bother with types in Python? Isn’t dynamic typing a feature that makes python a great language in the first place?”. While dynamic typing can be easier to code, there are some benefits to a more static typing approach. To name a few:

  • Static types help clarify what a function expects for parameters, and what it returns. You can look at a single line of code and immediately understand what will be returned by a function, without having to skim through and follow the code.

  • Static typing increases the power compiling/linting has to find errors before run-time.

  • It forces the developer to fully think through code design a bit deeper. Having to match up data types when chaining functions involves a bit more planning compared to just throwing generic data around.

Basic Type Hinting

By default, python is a strongly typed, dynamic language. This means that while Python does have types for everything, the system generally does all of this under the hood, without requiring the programmer to define and keep track of the types. However, ’type hinting’ provides the ability to define types in python code.

For an example, lets start by looking at this stupid simple function. It takes an age for input, and returns a string that states how old you will be in the future (it increment’s the age by 1 and adds it to a string 😑. I know, riveting code here…):

def next_year_age(age):
    new_age = age + 1
    return f"In the future, you will be {new_age}!"

It can be assumed that age is likely an int, and that the return value is a str… but thats not a given. So, lets declare the types using type hinting.

To use type hinting in python, just specify the type when defining variables using the format var_name: type. Additionally, to define a return type for a function, add a -> symbol at the end of the function def pointing to the return type. So, our example function would look something like this:

def next_year_age(age: int) -> str:
    new_age: int = age + 1
    return f"In the future, you will be {new_age}!"

Here we defined that the age parameter, as well as the new_age variable inside the function, are both ints. Additionally, we declared that the function expects to return a string, by using the -> str: in the function definition.

MyPy

When using type hinting, it is strongly suggested to use Mypy, an optional static type checker for python, which can be installed using pip:

pip install mypy

To try mypy out with our example function above, lets assume that it is saved in a file named example.py. To run a basic check:

(post_examples) ➜  post_examples mypy example.py
Success: no issues found in 1 source file

Looks good! Now, lets pretend we accidentally wrote that the function would return a float (even though it actually returns a str):

def next_year_age(age: int) -> float:
    new_age: int = age + 1
    return f"In the future, you will be {new_age}!"

Mypy will catch the error for us:

(post_examples) ➜  post_examples mypy example.py
example.py:3: error: Incompatible return value type (got "str", expected "float")
Found 1 error in 1 file (checked 1 source file)

Strict Checking

Up until now, mypy has just told us if there are any issues with the type hinting we added. It hasn’t been too pushy about ensuring we have types declared for everything.

To have mypy enforce that we write code with un-ambiguous types (and get the most out of static typing), we can use the --strict flag. Looking at our original example again, lets say we didn’t declare a return type at all:

def next_year_age(age: int):
    new_age: int = age + 1
    return f"In the future, you will be {new_age}!"

Running mypy normally won’t care about this omission, because none of the types we did declare are wrong:

(post_examples) ➜  post_examples mypy example.py
Success: no issues found in 1 source file

However, using the --strict flag on the same file will let us know that we forgot to define a return type:

(post_examples) ➜  post_examples mypy --strict example.py
example.py:1: error: Function is missing a return type annotation
Found 1 error in 1 file (checked 1 source file)

More Complex Types

If you need to use compound or more complicated types, you might have to import from the typing module to do so. For example, lets say we want to instead have the function return a list of the two ages. We can do this by importing List from typing, and then define the return type as a list of ints:

from typing import List

def next_year_age(age: int) -> List[int]:
    new_age: int = age + 1
    return [age, new_age]

If we want to instead use a 2-element tuple:

from typing import Tuple

def next_year_age(age: int) -> Tuple[int, int]:
    new_age: int = age + 1
    return (age, new_age)

and so on.

Lastly, to define your own class and type hint it:

from typing import Tuple

class Person:
    def __init__(self, name: str, age: int):
        self.name: str = name
        self.age: int = age

def next_year_age(age: int) -> Tuple[int, int]:
    new_age: int = age + 1
    me: Person = Person("Ryan", age)
    return (me.age, new_age)

mypy can check that as well:

(post_examples) ➜  post_examples mypy example.py --strict
Success: no issues found in 1 source file

The typing module also supports types like the Any, Callable, Unions and others.

Issues

As I progressed further in my project, I started hitting more and more limitations/frustrations specifically related to the type checking. Most often, it was related to the crazy work-around I had to do to support Optional parameters and variables. Optionals are supported, but a bit complicated.

The biggest issue I had was being able to tell the system “I checked, and definitely have this object, it’s not None”. I would check for a value and only run a case on it if it existed, but the type system would still refuse to run logic like < on a Optional(int) type. It didn’t catch on to the fact that I had wrapped the variable in a case that can only occur if the value was an int.

I eventually tried using the returns library, which was great and can apparently help get around these issues by introducing Maybe and Some types, but at that point I realized I had added SO much overhead complexity to a very tiny and simple side project, that it really didn’t make sense.

Conclusion

Type hinting in python is probably best if you want to sprinkle it throughout the code without --strict checking, or if you have something where everything is rather straight forward and there aren’t many ‘optional’ variables defined and passed around. For projects where you want really strict type checking… you should probably use a different language (ex: Rust or Go) instead of forcing Python to be something it wasn’t designed to be from the start.

Still, I appreciate that type hinting exists in Python and I think I’ll use it throughout my projects more often. If you haven’t used it before, I recommend giving it a try!

Next Post:
Prev Post:

Gnome Font Scaling Script Traded in my M1 MacBook Air for the 14" MacBook Pro