Python Type Hints - How to Use typing.cast()
Python’s dynamism means that, although support continues to expand, type hints will never cover every situation. For edge cases we need to use an “escape hatch” to override the type checker.
One such escape hatch is the
# type: ignore comment, which disables a type checking error for a given line. I previously covered managing such comments, and making them more specific so they do.
Another, preferable escape hatch we can use is casting. We can cast explicitly with
typing.cast(), or implicitly from
Any with type hints. With casting we can force the type checker to treat a variable as a given type.
Let’s look at how we can use explicit and implicit casting, and a Mypy feature for managing calls to
When we call
cast(), we pass it two arguments: a type, and a value.
value unchanged, but type checkers will treat the return value as the given type instead of the input type. For example, we can make Mypy treat an integer as a string:
from __future__ import annotations from typing import cast x = 1 reveal_type(x) y = cast(str, x) reveal_type(y) y.upper()
Checking this program with Mypy, it doesn’t report any errors, but it does debug the types of
y for us:
$ mypy example.py example.py:6: note: Revealed type is "builtins.int" example.py:8: note: Revealed type is "builtins.str"
But, if we remove the
reveal_type() calls and run the code, it crashes:
$ python example.py Traceback (most recent call last): File "/.../example.py", line 7, in <module> y.upper() AttributeError: 'int' object has no attribute 'upper'
Usually Mypy would detect this bug, as it knows
int objects do not have an
upper() method. But our
cast() forced Mypy to treat
y as a
str, so it assumed the call would succeed.
cast() really does do nothing - its special behaviour is only in how type checkers interpret it. Python’s source code reveals
cast() is a simple no-op function call:
def cast(typ, val): """Cast a value to a type. This returns the value unchanged. To the type checker this signals that the return value has the designated type, but at runtime we intentionally don't check anything (we want this to be as fast as possible). """ return val
cast() is a normal Python function, calling it does carry a very slight runtime performance penalty. This will very rarely be an issue. As usual, you should profile your code before making any assumptions about performance.
The main case to reach for
cast() are when the type hints for a module are either missing, incomplete, or incorrect. This may be the case for third party packages, or occasionally for things in the standard library.
Take this example:
import datetime as dt from typing import cast from third_party import get_data data = get_data() last_import_time = cast(dt.datetime, data["last_import_time"])
get_data() has a return type of
dict[str, Any], rather than using stricter per-key types with a
TypedDict. From reading the documentation or source we might find that the
"last_import_time" key always contains a
datetime object. Therefore, when we access it, we can wrap it in a
cast(), to tell our type checker the real type rather than continuing with
When we encounter missing, incomplete, or incorrect type hints, we can contribute back a fix. This may be in the package itself, its related stubs package, or separate stubs in Python’s typeshed. But until such a fix is released, we will need to use
cast() to make our code pass type checking.
It’s worth noting that
Any has special treatment: when we store a variable with type
Any in a variable with a specific type, type checkers treat this as an implicit cast. We can thus write our previous example without
import datetime as dt from third_party import get_data data = get_data() last_import_time: dt.datetime = data["last_import_time"]
This kind of implicit casting is the first tool we should reach for when interacting with libraries that return
Any. It also applies when we pass a variable typed
Any as a specifically typed function argument or return value.
cast() directly is often more useful when dealing with incorrect types other than
When we use
cast() to override a third party function’s type, that type be corrected in a later version (perhaps from our own PR!). After such an update, the
cast() is unnecessary clutter that may confuse readers.
We can detect such unnecessary casts by activating Mypy’s
warn_redundant_casts option. With this flag turned on, Mypy will log an error for each use of
cast() that casts a variable to the type it already has.
(This provides a similar type-cleanliness check to
warn_unused_ignores, which I covered previously.)
For example, take this unnecessary
from typing import cast x = 1 y = cast(int, x)
Running Mypy with the option active, we see this error:
$ mypy --warn-redundant-casts example.py example.py:6: error: Redundant cast to "int" Found 1 error in 1 file (checked 1 source file)
Activating this option is a great guard for keeping our types clean.
Improve your Django develompent experience with my new book.
One summary email a week, no spam, I pinky promise.
- Python Type Hints - How to Enable Postponed Evaluation With __future__.annotations
- Python Type Hints - Duck typing with Protocol
- Python Type Hints - How to Type a Context Manager
Tags: mypy, python