Type Hints in Python

Author:Murphy  |  View: 22722  |  Time: 2025-03-23 18:08:52

The other day I was trying to decipher how a script I built in the past worked. I knew what it did, it was pretty well explained and documented, but understanding the how was more troublesome.

The code was tedious and complex, with some comments here and there but lacking proper styling. That's when I decided to learn about PEP 8[1] and integrate it into my code.

In case you don't know what PEP 8 is, it's basically a document that provides guidelines, coding conventions, and best practices on how to write Python code.

The solution to our incomprehensible codes is right there. Yet most of us have never invested our time to read it and integrate those guidelines into our daily practices.

It takes time and a lot of mistakes, but trust me it's worth it. I've learned so much and my codes are now starting to look better.

One of my favorite findings was the Type Hints (or type annotations) – which will be the topic of today's post. In fact, type hints already appeared on PEP 3107[2], back in 2006, and were revisited and fully documented in the 484[3] version (in 2014). Since then, it's been improved several times on new PEP versions and it's almost become a classic.

So, an old topic yet very new for many.

What is Type Hinting?

Type hints indicate the datatypes of both the inputs and outputs in functions (it applies to class methods as well).

A problem many Python users complain about is the freedom we have to change a variable type. In other languages such as C and many others, you need to declare a variable specifying its type: char, integer…

Each one will have their own opinion – some might love Python's freedom (and its effects on memory management) and some others will prefer the restriction of old-school languages because it makes their code more readable.

Anyway.

Type hints are here to make your Python code more readable, an approach that I'm sure most of us appreciate. However, these are meant to clarify, they don't make the datatype a requirement for the variable.

If the variable's type isn't what we expect, no errors will be raised.

Why Should Data Scientists Consider Using Them?

To be honest, any Python programmer would benefit from type annotations. But it probably makes even more sense for data scientists and other data-related professionals.

That's because we work with all kinds of data. Not just the simple strings, lists, or tuples. We use data that can end up being about super complex structures and type hints have the potential to save us a lot of time trying to know what type of data was expected on a given function.

For example, let's imagine we have a structure that is dictionary-based. Its keys are tuples and its values are nested dictionaries with string keys and set values.

Good luck trying to remember that when you revisit the code some months later!

The good part is that type hints are extremely easy to understand and easy to use. We have no excuse not to use them, and there are no perks in not doing so.

So, let's go ahead and start seeing some code.

1. First Overview

I'll be using Python 3.11, but most of the examples will work on previous versions of Python 3.

Let's use a sample and dummy function:

def meet_someone(name, age):
    return f"Hey {name}, I heard you are {age}. I'm 21 and from Mars!"

This is stupid. But it has everything we need and we'll be adding variations just now.

We don't have any type annotations here. Just a function that takes two parameters and returns a string. I'm sure you know that the name parameter is supposed to be a string while the parameter age is expected to be an integer (or float even).

But you know it because it's a really simple function. It's hardly ever that simple. That's why adding some hints might be wise.

Here's the update:

def meet_someone(name: str, age: int) -> str:
    return f"Hey {name}, I heard you are {age}. I'm 21 and from Mars!"

In this case, I specified age should be an integer. Let's try to run the function:

>>> meet_someone('Marc', 22)
Hey Marc, I heard you are 22. I'm 21 and from Mars!

To illustrate what I said at the end of the previous section:

>>> meet_someone('Marc', 22.4)
Hey Marc, I heard you are 22.4. I'm 21 and from Mars!

It worked well even though 22.4 is a float (and not an integer which is expected). As said, these are just type hints, nothing more.

Okay, basics covered. Let's start making some variations.

2. Multiple Data Types

Suppose we want to allow both integers and floats as data types for the age argument. We can do so using Union, from the typing module[4]:

from typing import Union
def meet_someone(name: str, age: Union[int, float]) -> str:
    return f"Hey {name}, I heard you are {age}. I'm 21 and from Mars!"

It's simple: Union[int, float] means that we expect either an integer or a float.

However, if you find yourself using Python 3.10 or higher, there's another approach you can use to do the same without even using Union:

def meet_someone(name: str, age: int | float) -> str:
    return f"Hey {name}, I heard you are {age}. I'm 21 and from Mars!"

It's just a simple OR operator. Easier to understand in my opinion.

3. Advanced Data Types

Suppose now we were to play with more complex parameters such as dictionaries or lists. Let's now use the next function, which uses the meet_someone function:

def meet_them_all(people) -> str:
    msg = ""
    for person in people:
        msg += meet_someone(person, people[person])
    return msg

This is a really simple function still, but now the argument might not be as clear as we previously saw. If you actually inspect the code, you'll see we expect a dictionary.

But wouldn't it be better if we didn't have to guess? Again, that's the power of type hints.

At this point, if I asked you to add type hints yourself, you'd probably be doing something like this:

def meet_them_all(people: dict) -> str:
    msg = ""
    for person in people:
        msg += meet_someone(person, people[person])
    return msg

This is good. But we're not using its full potential. We're specifying we want a dict here but not the types of its keys and values. Here's an improved version:

def meet_them_all(people: dict[str, int]) -> str:
    msg = ""
    for person in people:
        msg += meet_someone(person, people[person])
    return msg

Here, we're saying that we expect people to be a dictionary with keys as strings and values as integers. Something like {'Pol': 23, 'Marc': 21}.

But remember that we want to accept ages as either integers or floats…

from typing import Union
def meet_them_all(people: dict[str, Union[int, float]]) -> str:
    msg = ""
    for person in people:
        msg += meet_someone(person, people[person])
    return msg

We can just use what we learned in section 2! Cool huh?

Oh, and it doesn't just work on the built-in data types. You can use any data type you want. For example, imagine we want a list of Pandas data frames for a function that doesn't return anything:

import pandas as pd

Vector = list[pd.DataFrame]

def print_vector_length(dfs: Vector) -> None:
    print(f'We received {len(dfs)} dfs')

What I did here was declare the data type, which is just a list of data frames, and use it as a type hint.

Also, something we haven't seen before today, this function doesn't return anything. That's why the output data type is None.

4. The Optional Operator

It's often that we create functions in which some arguments aren't required – they're optional.

Given all we've seen until now, here's how we could code a function with optional parameters:

def meet_someone(name: str, 
                 age: int | float, 
                 last_name: None | str = None
                ) -> str:
    msg = f"Hey {name}{' ' + last_name if last_name else ''}, "
          f"I heard you are {age}. I'm 21 and from Mars!"

    return msg

I've updated the returned message but the important part is the type hint for the last parameter, last_name. See how here I'm saying: "last_name is either a string or a Null value. It's optional and by default, it's a None."

This is cool and pretty intuitive, but imagine a parameter with several possible data types… It can get very long.

That's why the Optional operator is useful here, it basically allows us to skip the None hint:

from typing import Optional

def meet_someone(name: str, 
                 age: int | float, 
                 last_name: Optional[str] = None
                ) -> str:
    msg = f"Hey {name}{' ' + last_name if last_name else ''}, "
          f"I heard you are {age}. I'm 21 and from Mars!"

    return msg

Conclusion & Next Steps

I hope I've transmitted how useful type hints are to improve the readability and comprehension of our codes. Not just for our fellow programmers, but for our future selves too!

I've covered the basics here, but I suggest you keep inspecting what the typing module offers. There are several classes there that can make your code look better than ever.

Thanks for reading the post! 

I really hope you enjoyed it and found it insightful.

Follow me and subscribe to my mailing list for more 
content like this one, it helps a lot!

@polmarin

If you'd like to support me further, consider subscribing to Medium's Membership through the link you find below: it won't cost you any extra penny but will help me through this process.

Join Medium with my referral link – Pol Marin


Resources

[1] PEP 8 – Style Guide for Python Code | peps.python.org

[2] 3107 – Function Annotations| peps.python.org

[3] 484 – Type Hints| peps.python.org

[4] typing – Support for type hints

Tags: Data Science Python Python Programming Tips And Tricks Type Hints

Comment