Mypy Tips and Tricks
Here’s a collection of various tips and tricks for mypy, a static type checker for Python. Some are more opinionated than others, but they all will help you leverage mypy to its full potential.
Use a type checker
First off, if you’re using type hints in your code, I highly recommend you type check them with a tool like mypy. The only thing worse than unannotated code is incorrectly annotated code.
If you’re using mypy, I like the following
mypy.ini for my config:
[mypy] disallow_any_generics = true disallow_incomplete_defs = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true follow_imports = normal ignore_missing_imports = true no_implicit_reexport = true show_error_codes = true show_error_context = true strict_equality = true strict_optional = true warn_redundant_casts = true warn_return_any = true warn_unused_ignores = true
The rest of this post touches on most of these flags and the idea behind them.
Enable None checking
Help mypy catch the billion-dollar mistake and enable
None checking with the
--strict-optional flag. Luckily, this is the default in mypy starting from version
With this flag, mypy would disallow the following code:
def foo(x: int) -> None: ... foo(None) # None is not of type 'int`
I also recommend setting the
--no-implicit-optional flag, which disallows code like the following:
# wouldn’t pass with the flag def foo(x: str = None) -> str: ... # but would with an explicit Optional annotation def foo(x: Optional[str] = None) -> str: ...
In general, being explicit is better than being implicit. This is especially true as the codebase grows older and the number of developers touching it increases—i.e., programming in the large. The time it would take to fix any future bugs caused by an implicit optional would dwarf the few extra seconds it takes to be explicit when first writing the code.
Remember, code is read more often than it is written, and it is run more often than it is read.
Beware of “Any” types, especially silent ones
Mypy will pass any type checks that contain an
Any is a helpful escape hatch when retrofitting type hints onto an existing codebase. Unfortunately, it can also jeopardize the type safety of your program if it appears in places you aren’t aware of.
Any types can appear when using types from your dependencies if you have not configured mypy to follow imports. For example, if you import a method from a third-party dependency, then mypy will treat values returned from that method as
from dep import foo # dep is a third-party dependency def bar(s: str) -> int: ... x = True y = foo(x) bar(y) # this passes mypy regardless of the type of y
Note that even if
foo has type hints, mypy will still always pass the above line if it is not configured to follow imports. Luckily,
--follow-imports=normal is the default for mypy, but this option is often turned off when adding mypy to an existing codebase.
Unfortunately, this behavior is not always clear, and mypy can lull you into a false sense of confidence when it passes your code while in reality, it’s considering everything as
Any. Speaking of which …
Make sure your library has a py.typed file
PEP-561 specifies that if you want your package to export its type hints, then you must include a
py.typed marker file in your package installation. The file is just an empty file. If you do not include it, then your package’s users will not be able to leverage your type hints in their mypy runs. To them, it will be as if your package has no type hints at all.
This is a small but crucial detail that is all too easy to miss while you’re learning about Python’s
typing module. As the PEP states, you’ll also need to declare this
py.typed file as part of your package’s installation. For example, you can add it to the
package_data section of your
To confirm this behavior, we can see how mypy infers the type of an imported constant with
from dep import CONSTANT # CONSTANT = 1 reveal_type(CONSTANT)
$ mypy foo.py foo.py:1: error: Skipping analyzing 'dep': found module but no type hints or library stubs foo.py:1: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports foo.py:3: note: Revealed type is 'Any'
Mypy thinks it’s
Any, even though the actual value is the integer
1. But after we add an exported
py.typed file to our
mypy foo.py foo.py:3: note: Revealed type is 'builtins.int'
Mypy correctly infers the type as an
Consider using object instead of Any in some cases
Sometimes, you’re deserializing a big JSON blob or dealing with a dictionary with multiple different, complex value types. In cases like these, it may be tempting to reach for
Any as the type hint. The
object type hint is similar to TypeScript’s
object as a typesafe alternative to declaring that a value could be anything. Mypy won’t allow you to do most things with
object; you’ll have to narrow the type with
isinstance checks or similar:
def foo(x: object) -> int: return len(x) # mypy rejects this def foo(x: object) -> int: if isinstance(x, str): return len(x) # passes mypy as we've confirmed x: str elif isinstance(x, int): return x # passes mypy as we've confirmed x: int raise TypeError()
This is the key distinction between
Any to opt-out of static type checking, and use
object to declare that the value can be of any type.
Of course, if you’re declaring that a value can be of any type, then there’s not a lot of useful things you can do with that value until you introspect its type. As this blog post eloquently describes it, “you can’t process what you don’t know.”
While there is no distinction between
object at runtime, using
object in your code will let mypy force you to prove that you can safely use and manipulate your value before you do.
Any will not.
Tell mypy to show error context
--show-error-context flag directs mypy to give a couple of lines of context for every error.
Without the flag:
$ mypy bar.py bar.py:2: error: Argument 1 to "len" has incompatible type "object"; expected "Sized" Found 1 error in 1 file (checked 1 source file)
With the flag:
$ mypy bar.py --show-error-context bar.py: note: In function "foo": bar.py:2: error: Argument 1 to "len" has incompatible type "object"; expected "Sized" Found 1 error in 1 file (checked 1 source file)
Not a big deal (and the added verbosity could be unwanted in CI), but it’s helpful when you’re not as familiar with the code.
Enable “drift” checks
--warn-unused-ignores flags tell mypy to warn you if there’s an unneeded cast or unused
type: ignore comment.
Both of these usually happen over time as your code starts to drift. That is, when you first write the code, you might need the
cast(str, x) function to direct mypy a certain way. But later on, if you update another section of the code, then you may no longer need it.
It’s not always obvious when updates to one section of the codebase affect mypy’s understanding of another section. These flags help prune your code over time.
Mypy continues to get smarter as new versions are released, and previous
type: ignore comments due to mypy bugs or deficiencies can become stale over time. It is especially important to remove these when they’re stale, as they can prevent mypy from notifying you of different but valid type errors on the same line of code.
Disallow untyped definitions and method calls
If you’re going to adopt mypy on your codebase, it helps to go all in and annotate all your methods. The
--disallow-untyped-defs flags tell mypy to enforce this. Adding type hints to an existing codebase is hard. Luckily, there are some great tools to ease the transition, like typing_copilot.
Dealing with untyped methods causes mypy to treat types as
Any, passing all subsequent checks involving them. It is better to explicitly annotate your methods with
Any types than to leave them unannotated, as being explicit is better than being implicit. Also, this reduces the barrier to refine your type hints in the future.
These flags also prevent any new unannotated code from being added to the codebase, helping ensure your codebase doesn’t regress in this respect.
Disallow untyped decorators
Wrapping your typed method with an untyped decorator will cause your method to be untyped. Mypy understands the type hints in a regular typed function:
def foo() -> int: return 2 reveal_type(foo) # Revealed type is ’def () -> builtins.int'
If we decorate this method with an untyped decorator, mypy now infers the method as untyped:
def decorate(func): def inner(): return func() return inner @decorate def foo() -> int: return 2 reveal_type(foo) # Revealed type is 'Any'
We took care to add type hints to our functions. It would be a shame if we miss out on some benefits because we happen to decorate with untyped decorators.
--disallow-untyped-decorators flag directs mypy to warn whenever it encounters a typed method wrapped by an untyped decorator. This is a great way to prevent the introduction of some silent
Any types into your code.
No implicit re-export
Python allows you to import
foo from file A even if file A doesn’t declare
foo, provided that file A imports
foo from somewhere else. That is, the following situation is valid:
# File A from file_b import foo ...
# File B def foo(): ...
# File C from file_a import foo # Note that it's file_a, not file_b
Even though this works, this indirection increases the likelihood of problems in the future. If we refactor file A so that it no longer imports
foo, then file C will also stop working.
--no-implicit-reexport flag directs mypy to warn us whenever it detects this situation.
With this flag, we can still export the imported
foo from file A with either of the following:
# File A from file_b import foo as foo
# File A from file_b import foo __all__ = ["foo"]
You’ll receive benefits from this flag even if you rely on exporting imported values as it forces you to make them explicit.
Disallow implicit type parameters in generics
By default, mypy allows you to use
Dict as type hints without specifying type parameters. Mypy uses
Any for all type parameters you don’t specify, so
List is equivalent to
Dict is equivalent to
Dict[Any, Any]. Starting with Python 3.9, you can specify type parameters with the built-in collections, like
list[str]. See PEP-585 for more details.
--disallow-any-generics flag directs mypy to reject any generic type hints without explicit type parameters.
You start to develop a healthy distrust of
Any as you work more with Python type hints.
Any has legitimate uses, but you’ll correctly double-check the context when you see one. This increased caution is good since mypy will gloss over
Enabling this flag continues our trend of preferring explicitness. Generics without type parameters are a great place for
Any types to hide.
Dict[Any, Any] will catch your eye—while
Dict may not.
Enable a few more warnings
--warn-return-any flag tells mypy to produce a warning if you’re returning
Any from a method declared to return another type.
--warn-unreachable flag tells mypy to produce a warning if it detects code that will never execute.
--strict-equality flag tells mypy to flag any comparisons between non-overlapping types. These always evaluate to false at runtime.
All the conditions that trigger these warnings are often the result of buggy code or silent
Any types. These warnings, along with the other tips discussed in this post, will help you better leverage mypy to create more robust and maintainable software.