Generally, people think of Python as a messy language because it lacks explicit typing and static type checking. But, that’s not quite true in modern times. Surely, we can take advantage of type hints!
Table of Contents
- Concept Overview
- Which Versions of Python Support Type Hints?
- What If I Am Not On the Latest Version of Python?
- What All Can Be Type Hinted?
- Not So Subtle Hints
Concept Overview
Most modern programming languages have some form of type system. The idea being that data can be categorized and manipulated based on its type. Of course, every programming language seemingly has a different type system, so the way data types are managed and handled are also different.
In Python, the type system is often described as “duck typing,” falling from that idea that “if it walks like a duck and quacks like a duck, it must be a duck.” What that means in the context of Python is that we never have to explicitly type our variables. As long as the variable behaves like a particular type, it must be that type.
Of course, if you come from a more explicit typing background like Java or C#, then you may find Python difficult to use. After all, it can be difficult to tell what type a variable is at a glance. Fortunately, you’re not forced to live without explicit types in Python. Instead, you can make use of type hints.
As the name suggests, a type hint allows you to document the type of a variable directly through Python syntax. For example, we might have a silly function which flips a coin and lets the user know if they selected the correct side:
import random def flip_coin(side): flip = "heads" if random.random() > .5 else "tails" if side == flip: print("Congrats! You win!") else: print("Unlucky")
While it’s probably clear what type side
and flip
are at a glance, it’s better to be a bit more explicit by adding some type hints:
import random def flip_coin(side: str): flip: str = "heads" if random.random() > .5 else "tails" if side == flip: print("Congrats! You win!") else: print("Unlucky")
Of course, I would probably verify the input is valid too, but the point still stands.
With that said, it’s important to note that type hints do not actually do anything. Their only purpose is to aid with documentation and third-party tooling (e.g., IDEs, linters, etc.). However, those reasons alone should be enough to convince you start using them.
As always, in the rest of this article, we’ll take a look at some questions you might have about type hints.
Which Versions of Python Support Type Hints?
Type hinting is a Python feature that has been slowly maturing since they were introduced in Python 3.5 via PEP 484—roughly a decade ago. Prior to Python 3.5, the only way to get proper static type checking with tooling was to follow that tool’s conventions.
At the time, type hints in Python 3.5 were very limited to function parameters and return types. Therefore, to get variable-level type hints, you had to make use of comments:
numbers = [] # type: List[int]
Fortunately, in Python 3.6 via PEP 526, the type hint syntax was extended to allow inline type hints for variables:
numbers: List[int] = []
Unfortunately, these were still a little clunky because Python couldn’t handle many of the built-in types. As a result, to get the behavior shown above, the List type would have to be imported:
from typing import List
This was fortunately changed in Python 3.9 via PEP 585, so the following is now possible:
numbers: list[int] = []
It was a subtle change but a welcome one for folks who wanted to make use of type hinting.
Similarly, you may recall in the flip_coin
example that we could specify that the parameter must be a string. Well, since Python 3.8 via PEP 586, we can now specify exactly what literal values we will accept:
from typing import Literal def flip_coin(side: Literal["heads", "tails"]): ...
By Python 3.10, a host of wonderful features were added. For example, courtesy of PEP 604, if a function input can take on multiple types, then you can specify that using the union operator:
def busy_func(data: int | str | list): ...
This is particularly handy in situations where you want a parameter to be optional, which can be specified in one of two ways (of course, keep in mind that one requires an explicit import):
def optional_params1(data: Optional[int]): ... def optional_params2(data: int | None): ...
Until Python 3.11, type hints were still clunky in a few areas. For example, if you wanted to reference the type of the class you were defining inside that class (i.e., self-referential), most type checkers would say that the type was not yet defined. As a result, a string would have to be used as a forward reference:
class Node: def connect(next: "Node"): ...
Obviously, this is sort of clunky, so PEP 673 included the Self type:
from typing import Self class Node: def connect(next: Self): ...
By Python 3.12, folks were realizing the power of the type system, but it had one major drawback. For sufficiently complicated types, APIs would be littered with type hints. To solve that problem PEP 695 introduced a way of creating better type aliases using the type keyword. Now, if you have a situation where you have messy nested structures, you can capture that structure in an alias:
type DictMatrix = list[list[dict[str, str]]] def get_game_grid() -> DictMatrix: ...
Ultimately, I expect the type hinting system to continue to mature. Fortunately, even if this article goes out of date, Python lists all of their changes to the type system for your perusal.
What If I Am Not On the Latest Version of Python?
While type hints are already a decade old at this point, the type hinting system receives annual updates. This means that you may not get access to the latest features if you’re on an older version of Python. By convention, it is a good idea to update your Python version regularly. After all, the Python community only supports about five versions of Python at a time. At the time of writing, the currently supported versions are 3.9, 3.10, 3.11, 3.12, and 3.13. 3.9 will reach end of life right around the time 3.14 is released (i.e., October 2025).
With that said, if you absolutely cannot update your version of Python, modern type hinting can be accessed by “importing” it directly. The trick Python developers use for this is the following:
from __future__ import annotations
According to the docs, this is known as a future statement, and it essentially forces the compiler to support newer syntax and/or semantics. Currently, the only way to execute the future statement above is to be on Python 3.7 or newer. I do not know exactly which type hinting features are included and which are not (maybe this is a nonsensical concern), and I was unable to figure it out by browsing the docs. Feel free to browse PEP 563 yourself.
What All Can Be Type Hinted?
While the concept overview may have implied that type hints are only available for variables, they can actually be used in a lot of important locations. For example, function parameters and return types can be labeled:
def sum(a: int, b: int) -> int: ...
For a complete list of all of the typing features, check out the typing API.
Not So Subtle Hints
Ultimately, type hinting is a feature I use basically any time I use Python now. In fact, I am often upset when I look at an API, and there aren’t type hints. It makes it much more difficult to figure out exactly what a function accepts or what it’s going to return. This is particularly true for APIs where I have to run the functions to figure out what they do (e.g., the Canvas API). Meanwhile, APIs like PRAW and Discord.py are much nicer to use because of the type hints.
With that said, I think we’ll call it here for today. As always, if you liked this article, there are plenty more like it:
- Obfuscation Techniques: No More Type Hints
- Unpacking CS Jargon: Static Vs. Dynamic Types
- The Self-Taught Guide to Type Systems in Python
Likewise, here are some useful Python resources (#ad):
- Effective Python: 90 Specific Ways to Write Better Python
- Python Tricks: A Buffet of Awesome Python Features
- Python Programming: An Introduction to Computer Science
If you want, you can even help support the site in other ways such as through the Patreon or Discord. You’ll just need to head over to our list of ways to grow the site. Otherwise, take care!
Recent Code Posts
Unpacking the Jargon Around Compilers, Interpreters, and More
Today, we're going to get a little pedantic and try to define concepts like compiler and interpreter. Of course, I ultimately don't think it matters what the exact definitions are for practical...
It's a special day when I cover a Java topic. In this one, we're talking about Enums, and the problem(s) they are intended to solve.