Get started with Python type hints

Learn how to use Python’s optional type hinting syntax for creating cleaner and more useful code bases.

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 for 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 code base, and perform static analyses 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 it 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. In fact, by the time your program runs, all the type information you’ve provided has been erased. Python type hints are only 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 had experience with languages where type declarations are 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 code base 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 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.

But these projects are more properly thought of as complements to the core Python language rather than signs of where Python is intended to go. 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. An example:

name: str
age: int

name = input("Your name?")
age = 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.

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.

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. Here is how we could disambiguate that with 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 in 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 will sometimes want to type hint them to indicate what kinds of objects they contain. For this we need to turn to Python’s typing module, which supplies tools for describing the types such things will hold.

from typing import Dict, List

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 providing them as a list to typing.Dict. And you can describe the object type for a list by supplying that type to typing.List.

Optional and Union types

Some objects may 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.

Type hinting and classes

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

from typing import Dict

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 print()s 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 this illustration.)

When using type hints for custom objects, we will sometimes need to provide a type hint for an object that hasn’t been defined yet. 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 is useful if you have objects that have interdependencies, as in the above example. There is probably a more elegant way to untangle this, but at least you can provide ahead-of-time hints in the same namespace simply by giving the name of the object.

Copyright © 2021 IDG Communications, Inc.

How to choose a low-code development platform