Get started with Python type hints

Learn how to use Python’s optional type-hinting syntax to create cleaner and more useful code.

Get started with Python type hints
Getty Images

Python is best thought of as a dynamic but strongly typed language. Types aren’t associated with the names of things but with the things themselves.

This makes Python flexible and convenient for developers because you don’t have to rigorously define and track variable types if you’re just throwing together a quick-and-dirty script. But for bigger projects, especially libraries used by third parties, it helps to know which object types are associated with which variables.

For some time now, Python has had the ability to “annotate” names with type information, in one form or another. With Python 3.5, type hints officially became part of the language (PEP 484). Using a linter or code-checking tool, developers can check the consistency of variables and their types across a codebase, and perform static analysis of code that would previously have been difficult or impossible. All this is done ahead of time, before the code runs.

In this article, we’ll explore some basic examples of Python type hinting. But first, we’ll clear up a common misunderstanding about what type hinting is and isn’t used for.

How Python uses type hints (it doesn’t)

A key misconception about Python type hints is how they are used. Python type hints are not used at runtime, at least not ordinarily. In fact, by the time your program runs, all the type information you’ve provided has been erased. Python type hints are used ahead of time, by the type checking system you’re employing; for instance, in your editor or IDE. In other words, Python’s type hints are for the developer, not for the runtime.

This may sound counterintuitive, especially if you’ve worked with languages where type declarations were not optional. But Python’s development team has gone out of its way to make clear that type hints aren’t a prelude to the core Python language becoming statically typed. They’re a way for developers to add metadata to a codebase to make it easier to perform static analysis during development.

Some have speculated that Python type hinting could in time give rise to a fork of the language that is statically typed, perhaps as a way to make Python faster. In some ways this fork has already arrived: Cython uses type hints (although mostly its own peculiar breed of them) to generate C code from Python, and the mypyc project uses Python’s native type hinting to do the same. And projects like Codon or Mojo explore using type declarations to create a speedier alternative dialect of Python.

But these projects are more properly thought of as complements to the core Python language rather than signs of where Python is headed. The main purpose of type hinting in Python is to give developers a way to make their code as self-describing as possible, both for their own benefit and that of other developers.

The syntax of Python type hints

Type hints in Python involve a colon and a type declaration after the first invocation of a name in a namespace. Here's an example:


name: str
age: int

name = input("Your name?")
age = int(input("Your age?"))

# alternatively, and more simply:

name: str = input("Your name?")
age: int = int(input("Your age?"))

The first declarations of name and age with type hints ensure that any future use of those names in that namespace will be checked against those types. For instance, this code would be invalid:


name: int
age: int

name = input("Your name?")
age = int(input("Your age?"))

Because we declared name as an int already, and input by default returns a string, the type checker would complain. The code will still run, however, since type hints are not used at runtime, and outside of the mis-hinted types the code doesn't actually do anything wrong.

Python type-checking systems will, whenever possible, try to infer types. For instance, let’s say we used the following code without the previous type declarations:


name = input("Your name?")
age = int(input("Your age?"))

In that case, the type checker would be able to infer that name is a string (since input() doesn’t return anything else), and that age is an int (since int() doesn’t return anything else). But the best results come from hinting each variable explicitly, since it isn't always possible to make these inferences.

Type hinting Python functions

Python functions can also be type hinted, so that the values they accept and return are documented ahead of time. Consider the following code:


greeting = "Hello, {}, you're {} years old"

def greet(user, age):
    return greeting.format(user, age)

name = input("Your name?")
age = int(input("How old are you?"))

print(greet(name, age))

One ambiguity with this code is that greet() could in theory accept any types for user and age, and could return any type. We could disambiguate that confusion using type hints:


greeting = "Hello, {}, you're {} years old"

def greet(user: str, age: int) -> str:
    return greeting.format(user, age)

name = input("Your name?")
age = int(input("How old are you?"))

print(greet(name, age))

Given these type hints for greet(), your editor could tell you ahead of time which types greet() will accept when you insert a call to it into your code.

Again, sometimes Python can automatically infer what types are returned from a function, but if you plan on using type hinting with a function, it’s best to hint everything about it—what types it takes in as well as what types it returns.

Type hinting container objects

Because objects like lists, dictionaries, and tuples contain other objects, we sometimes want to type hint them to indicate what kinds of objects they contain. Previously, Python required using the typing module to provide tools for describing such types, but they can now be done natively in Python as of recent versions.


dict_of_users: dict[int,str] = {
    1: "Jerome",
    2: "Lewis"
}

list_of_users: list[str] = [
    "Jerome", "Lewis"
]

Dictionaries are made of keys and values, which can be of different types. You can describe the types for a dictionary by hinting with dict[<key type>, <value type>]. And you can describe the object type for a list with a hint in the format of list[<element type>].

Optional and Union types

Some objects contain one of a couple of different types of objects. In these cases, you can use Union or Optional. Use Union to indicate that an object can be one of several types. Use Optional to indicate that an object is either one given type or None. For example:


from typing import Dict, Optional, Union

dict_of_users: Dict[int, Union[int,str]] = {
    1: "Jerome",
    2: "Lewis",
    3: 32
}

user_id: Optional[int]
user_id = None # valid
user_id = 3 # also vald
user_id = "Hello" # not valid!

In this case, we have a dictionary that takes ints as keys, but either ints or strs as values. The user_id variable (which we could use to compare against the dictionary’s keys) can be an int or None (“no valid user”), but not a str.

In the most recent versions of Python, Union types can be hinted using the pipe character as follows, with no imports needed:


dict_of_users: dict[int, int|str] = {
    1: "Jerome",
    2: "Lewis",
    3: 32
         }

Also note that Optional can be hinted as a Union with None; for example, int|None instead of Optional[int]. This saves you the need to import Optional if you'd rather keep things simple.

Type hinting and classes

To provide type hints for classes, just reference their names the same as you would any other type:


class User:
    def __init__(self, name):
        self.name = name

users: dict[int, User] = {
    1: User("Serdar"),
    2: User("Davis")
}

def inspect_user(user: User) -> None:
    print (user.name)

user1 = users[1]
inspect_user(user1)

Note that inspect_user() has a return type of None because it only prints output and does not return anything. (Also, we’d normally make such a thing into a method for the class, but it’s broken out separately here for the purpose of illustration.)

When using type hints for custom objects, we sometimes need to provide a type hint for an object that hasn’t yet been defined. In that case, you can use a string to provide the object name:


class User:
    def __init__(self, name: str, address: "Address"):
        self.name = name
        self.address = address
        # ^ because let's say for some reason we must have
        # an address for each user

class Address:
    def __init__(self, owner: User, address_line: str):
        self.owner = owner
        self.address_line = address_line

This approach is useful if you have objects with interdependencies, as in the above example. There is probably a more elegant way to untangle it, but at least you can provide ahead-of-time hints in the same namespace simply by providing the name of the object.

Copyright © 2023 IDG Communications, Inc.