What Are Python Type-Hints and How to Use Them?

After covering SQL databases with Python and Flask, here and here, today I wanted to introduce the concept of type-hints in Python, their use cases, as well as their pros and cons. Type-hints can be great tools to improve the readability of your code but they are not always the best solution.

This post is intended as a beginner introduction to this topic, and I might write more if there is more interest. As a result, I expect this to be an easy and quick read. If you have any comments or suggestions, let me know in the comment section below.

What Are Python Type-Hints?

Original PEP 484 proposal for introducing Python type-hints back in Python 3.5
PEP 484 proposal

Python type-hints were first introduced in PEP 484 and came out with Python 3.5. As the name implies, type-hints are “hints” about the type of variables or objects in your code.

They allow you to declare the type of variables and return types of your functions. However, this does not mean that type-hints make Python statically typed. They are simply “hints”, meaning that they don’t affect how your program runs.

If they don’t have an effect on the code, why should we use type-hints? The short answer is that they can be useful for self-documenting code. Additionally, linters and IDEs can use them to improve their knowledge of your code.

Examples

Although type-hints have been available for a while, each new version of Python adds new features. The following examples were written with Python 3.9.4 in mind. If you get errors when running the code, maybe check that you are using a compatible release first.

Annotating Simple Variables

The first example will be about annotating basic python types in variables:

x:int = 10
f: float = 0.4
name:str = "hello"

As you can see, we are telling the IDE that x is an int variable, f is a float, and name is a str. The basic syntax for a type hint is to type your variable name followed by a colon “:” and then a basic Python type.

However, remember that type-hints are merely a suggestion and not enforced when the code is running (although particular runtime environments might be able to enforce them, I think). I could easily change the values in the variables like this:

x: int = 100

# I can assign any value to x
x = "Janice"

I can also set an incorrect value from the start:

x:int = "this is a string"

As you can see above, there is nothing stopping me from ignoring the type-hints. However, the idea is that if you are using them, it will force you to be more mindful about the types of your variables.

Finally, you can even annotate variables before you assign a value to them:

a:int   #a has no value yet

# ... do something here
my_str = "this is a string"
a = len(my_str)    #we only now assign a value to a

Annotating Container Objects (Dicts, lists, Tuples, etc)

In my opinion, annotating container objects is where type-hints can get messy and sometimes even take away from readability. Let’s start with a simple example, a list variable:

from typing import Union
# here I'm stating that my_list will only contain str and int values
my_list: list[Union[str, int]]

In the above example, you can see that we are declaring that the list can contain strings as well as integers.

The next example is for annotating Python dictionaries. In the case of dicts, there are two elements that need to be annotated: keys, and values. Here is the syntax example:

# simple annotation where keys and values are strings
simple_dict: dict[str, str] = {
    'city'    : 'London',
    'Country' : 'UK',
    'name'    : 'James'
}

In this example, our dict’s keys will be strings as well as their values. However, often we store many types of values in a dictionary. This is how to annotate those cases:

# use "typing" module
from typing import Union, Any

my_dict: dict[str, Union[str,int,float,None,Any]] = {
    'name'   : 'James',
    'age'    : 20,
    'height' : 1.8,
}

In the example, we are stating that the dictionary values can be: str, int, float, None, or Any. We use the Union import to indicate that the value can be any of the options indicated. The Any type is a catch-all expression that is compatible with all other types. It basically indicates that any type is acceptable.

I must also mention that on Python 3.10, following PEP 604, you can directly annotate several types without importing Union. Instead, you can use the Union shortcut “|”, as in the following example:

# annotation for Python 3.10
my_dict: dict[str, str|int|float|None|Any] = {
    'name' : 'James',
    'age' : 20,
    'height' : 1.8,
}

Function Annotation

Type-hints can also be used to annotate functions and their return types. Here is an example:

from typing import Union, NoReturn

#add ints or floats
def add_numbers(a:Union[int,float], b:Union[int,float]) -> Union[int, float]:
    return a + b

#function that does not return anything
def good_morning(name:str) -> NoReturn:
    print(f"Good morning {name}!")

In the first case, I’m again making use of the typing module to import Union. The second example uses NoReturn to indicate that the function does not return a value.

Those examples cover basic function annotations. Additionally, you can use the following syntax to indicate optional parameters in a function:

from typing import Optional

#function with optional parameters
def some_function(a:int, b:str, c:Optional[Union[int,float]]) -> int:
    pass 

In this case, we use Optional to indicate that the value can be any of the mentioned ones or None. It is basically equivalent to doing: Union[int, float, None].

Custom Classes and Types

Besides using built-in Python types for annotations, it would be great if we could use our own custom classes as type-hints. Fortunately, we can do just that. Let’s say we have a class SimpleClass and we want to declare a variable to an instance of the class. We can do it this way:

class SimpleClass(object):
    number = 10
    text   = "SimpleClass"

a: type[SimpleClass] = SimpleClass()

So, by using the type keyword, we can specify a custom type class. It will work also for functions when we need to return that class or take it as a parameter:

def alter_class(my_class: type[SimpleClass]) -> type[SimpleClass]:
    my_class.name = 20
    return my_class

In that example, the function alter_class takes an object of type SimpleClass and returns the same type of object.

Pros and Cons

pile of folders representing code documentation as metaphor for what Python type-hints can help with
documentation files

Now that I have covered some of the basic type-hints available, I wanted to mention their pros and cons.

Type-Hint Pros

  • Push you to think more carefully about your code and you variables
  • IDEs and linters will understand your code better, and could even warn you if you made a mistake.
  • Type-hints are a quick way to have self-documenting code

Cons

To be honest, the main disadvantage of type hints, in my opinion, is that when overused, they can clutter the code and make it hard to read. I have not shown examples of that happening. Regardless, you could imagine how annoying it would be to have those hints everywhere in the code.

My Personal Opinion

I have not used type-hints very frequently but I’m getting around to it, little by little. On one hand, when used correctly, they can add a lot of clarity when re-reading what I wrote. On the other hand, I don’t like that they are mere suggestions. I would be much more comfortable taking the time to use them if I knew that they would be respected. I think that one can easily forget that those variables annotated can still take any value at any point. Hopefully, IDEs can help will that.

In my code, I tend to have rather long and descriptive names. I feel they usually convey my intention pretty well. However, I also understand that type-hints provide a more standardized way of documenting our code.

And I certainly see the benefit in using type-hints in large code projects. Especially ones that are maintained by many different people, at different times. For quick throwaway scripts, it is probably not worth it too much.

Final Thoughts

Today I provided a basic introduction to Python type-hints. They have been around for a while, but I don’t see them used a lot yet, so I wanted to explain them here.

Leave a comment below with any suggestions or opinions on this post. I really appreciate it. Also, don’t forget to subscribe to the newsletter if you would like to stay in touch.

Have anything in mind?