Python Type Hints - Old and new ways to write the same types

This is how evolution works, right?

As type hints have evolved, Python has added simpler, more succinct syntaxes. But you still need to know the older forms, since Mypy uses them when reporting types.

Union types

A union type combines several types, expressing that a value may be any one of those types. PEP 484 introduced type hints and defined Union to express union types, allowing you to write:

from typing import Union

age: Union[int, float]

Later, PEP 604 added the | operator for union syntax. This allows you to skip an import of Union and instead write:

age: int | float

Cool.

This feature was added in Python 3.10. But you can use it from Python 3.7, if you use Mypy 0.800+ and postponed evaluations with from __future__ import annotations.

Optional types

An optional type is basically a union between a type and None. The value is either there, or None.

The first way to write this is with Union:

from typing import Union

name: Union[str, None]

Since optional types are very common, the original type hint PEP 484 added a shortcut, typing.Optional:

from typing import Optional

name: Optional[str]

But since PEP 604, it’s preferable to use the neat union syntax:

name: str | None

Nice.

(This may even become str? in the future, if Hynek Schlawack inebriates the right people.)

Lists and other builtin containers

When type hints were first introduced, they avoided disruption as much as possible. To avoid altering common types, the typing module introduced container types for builtin types. For example, you can represent a list of ints with typing.List:

from typing import List

years: List[int]

PEP 585 later merged that capability back onto the builtin types. This allows you to write:

years: list[int]

This applies to maany types:

You can see the full list in the PEP.

These changes were added in Python 3.9. But again, you can use them from Python 3.7, if you use Mypy 0.800+ and postponed evaluations with from __future__ import annotations.

pyupgrade can help you adopt the new syntax

The pyupgrade tool automatically rewrites code to use the latest Python syntax. It has support for adopting all the above new syntax, including changing imports. Check out my previous post for more details.

Mypy reports using old syntax

It’s nice to use the latest and greatest syntax when writing your code. But when Mypy reports types, it often still uses the old syntax (at least as of Mypy 0.982), so you still need to know how to read it.

Take this combined example, using reveal_type() to report the types:

age: int | float
name: str | None
years: list[int]

reveal_type(age)
reveal_type(name)
reveal_type(years)

Mypy says:

$ mypy example.py
example.py:5: note: Revealed type is "Union[builtins.int, builtins.float]"
example.py:6: note: Revealed type is "Union[builtins.str, None]"
example.py:7: note: Revealed type is "builtins.list[builtins.int]"
Success: no issues found in 1 source file

Note the differences:

Mypy can also report Optional, e.g. for this misassignment:

name: str | None = 123

…it says:

$ mypy example.py
example.py:1: error: Incompatible types in assignment (expression has type "int", variable has type "Optional[str]")
Found 1 error in 1 file (checked 1 source file)

So, at least for now, you need to know the old syntax to understand Mypy’s output.

I’d like to see Mypy updated to report types in the newer, shorter forms. I couldn’t find a specific issue, but I would guess there’s no update right now since Mypy supports older Python versions (when not using __future__.annotations). Also I saw there are some lingering edge cases with the PEP 604 union syntax.

Fin

May your type hints be succinct and your understanding comprehensive,

—Adam


Make your development more pleasant with Boost Your Django DX.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: ,