Python does not check types at runtime. A variable can hold an integer one moment and a string the next, and the interpreter will not complain until something actually breaks. mypy sits one layer above all of that — reading your source code before it ever runs and flagging type mismatches the way a compiler would in a statically typed language, without changing how Python itself behaves.
The relationship between mypy and Python is intentionally asymmetric. Python is dynamically typed by design — that flexibility is a feature, not an oversight. mypy does not try to eliminate that flexibility. Instead, it introduces a second layer of analysis, entirely separate from the interpreter, that checks the type contracts you choose to document in your code. If you annotate a function, mypy holds you to that annotation. If you leave something unannotated, mypy largely steps back — unless you configure it to push harder.
This article examines precisely how that enforcement works, from the initial source parse through type inference, gradual adoption strategies, strict mode configuration, and the control flow analysis that makes type narrowing possible. It also covers the concrete changes shipped in mypy 1.16 through 1.20, the current stable release series as of April 2026.
01 — rationaleWhy a Dynamic Language Needs a Static Checker
Python's dynamic type system means type errors surface at runtime, and only on the specific code path that actually executes. A function that receives the wrong argument type may pass an entire test suite that never exercises that branch, then fail in production under a condition nobody anticipated. This is not a theoretical problem — it is one of the most common categories of bugs in large Python projects.
The mypy project documentation frames the core value clearly: because Python is a dynamic language, type errors in unannotated code only surface when the faulty path actually executes at runtime, while mypy catches those same errors through static analysis before any code runs. — mypy-lang.org
The payoff is catching a specific class of errors earlier — at a point in the development cycle when fixing them is cheap and fast. For teams working on large Python projects, running mypy in CI before the test suite means certain bugs never reach testing at all. Beyond bug prevention, mypy also functions as machine-checked documentation. A function signature annotated with type hints tells any reader exactly what types are expected and what type comes back, with the guarantee that the annotation has been verified against the actual implementation.
The mypy FAQ is candid about when a project benefits most from this tooling, noting that mypy is particularly well-suited to codebases that are large or complex, maintained over a long time, or worked on by multiple developers — contexts where the cost of ambiguous interfaces compounds quickly.
02 — internalsHow mypy Works Under the Hood
When you run mypy mymodule.py, mypy does not execute your code. It parses the source file into an abstract syntax tree (AST), constructs a symbol table, and then runs type inference and type checking across that representation.
The process moves through several distinct phases. First, mypy resolves imports — it needs type information from every module your code touches. That information comes from type annotations in the source, from stub files (.pyi files), or from typeshed, the bundled repository of stubs for the standard library and many popular third-party packages. Second, it performs type inference: for unannotated code, mypy attempts to determine types from how values are assigned and used. Third, it checks those inferred and annotated types against one another, flagging every mismatch it finds.
mypy does not prevent your Python program from running. You can always execute annotated code with the standard interpreter even when mypy reports errors. The annotations are metadata — informative to mypy and to developers, but entirely invisible to the runtime.
A concrete example makes the mechanism clear:
# greet.py
def greet(name: str) -> str:
return "Hello, " + name
greet(42)
# greet.py:6: error: Argument 1 to "greet" has incompatible type "int"; expected "str" [arg-type]
The Python interpreter would raise a TypeError at runtime when that last line executes. mypy catches it without running a single line of code. The annotation name: str established a contract; passing 42 violates it, and mypy flags the violation at the call site — precisely where the developer needs to act.
mypy's type system supports both nominal subtyping (class hierarchy relationships) and structural subtyping (duck typing via Protocol). It primarily uses nominal subtyping, with structural subtyping available as an opt-in, because nominal typing produces shorter and more informative error messages and aligns with how Python developers already think about isinstance() checks.
mypy ships binary wheels compiled with mypyc, its own ahead-of-time compiler that translates Python to C extension modules. When you install mypy via pip install mypy, you typically get the mypyc-compiled wheel automatically. According to the mypy documentation, this makes mypy 3–5x faster than the pure-Python interpreter would. As of mypy 1.20, mypyc-accelerated wheels are available for Linux (x86-64 and ARM64), macOS (x86-64 and ARM64), and now Windows ARM64 and free-threaded Python 3.14.
reveal_type(items) in your source file and run mypy. What happens at runtime when you execute the script with the Python interpreter?03 — adoptionGradual Typing: Adopting mypy Without Rewriting Everything
One of mypy's most important design decisions is its support for gradual typing. The official documentation describes gradual typing as a core design goal: type hints can be added incrementally to an existing codebase, and unannotated code remains valid — the static and dynamic worlds coexist rather than compete. (mypy-lang.org)
In practice this means two things. First, unannotated functions are treated permissively by default — mypy will not flag calls into them unless you explicitly ask it to. Second, the special type Any acts as an escape hatch: a value typed as Any is compatible with every other type in both directions, effectively turning off checking for that value.
from typing import Any
def legacy_function(data): # unannotated: mypy largely ignores the body
return data["value"]
def typed_function(data: Any) -> int: # Any: explicitly opted out of checking
return data["value"]
def safe_function(data: dict[str, int]) -> int: # fully checked
return data["value"]
This graduated approach is what makes mypy viable for existing projects. A team can annotate their most critical modules first, get immediate value from type checking in those areas, and expand coverage incrementally without ever having to pause development for a big-bang migration. The python tutorials on this site follow the same philosophy — introducing concepts in layers so they build on each other.
Use mypy --ignore-missing-imports when first adding mypy to a project with many third-party dependencies that lack stubs. This prevents noise from libraries outside your control and lets you focus on errors in your own code first.
04 — configurationStrict Mode and Configuration
By default, mypy is permissive. That permissiveness is useful during adoption but becomes a liability once a codebase reaches meaningful annotation coverage. The answer is strict mode, enabled with a single flag or configuration line.
# pyproject.toml
[tool.mypy]
strict = true
Strict mode bundles together a set of flags that tighten enforcement significantly. The most consequential among them are described in the table below.
Any into typed code.list or dict must be parameterized — e.g. list[str], not bare list.list is equivalent to list[Any] and silently loses all element-level type safety.Any.Any from quietly contaminating callers — one unguarded return can silently disable checking across an entire call chain.None must be typed as T | None explicitly, not just T.None is a valid input.For projects migrating from an unannotated codebase, enabling all of these at once is usually impractical. The recommended path is to enable them incrementally — one flag at a time, fixing errors before moving to the next. The mypy blog explicitly notes that the --allow-redefinition-new flag introduced in 1.16 and the general trend toward stricter defaults are part of a planned path toward mypy 2.0, where several of today's opt-in behaviors will become the default.
Configuration lives in pyproject.toml, mypy.ini, or setup.cfg. Per-module overrides are supported, which is essential for realistic projects:
# pyproject.toml
[tool.mypy]
strict = true
# Relax rules for a legacy module still being migrated
[[tool.mypy.overrides]]
module = "myapp.legacy.*"
disallow_untyped_defs = false
warn_return_any = false
# Suppress errors from an untyped third-party library
[[tool.mypy.overrides]]
module = "some_untyped_lib.*"
ignore_missing_imports = true
05 — importsThird-Party Libraries: Stubs, py.typed, and Missing Imports
One of the first problems practitioners encounter after setting up mypy is the error that appears when importing a library that has no type information:
# mypy output when a library has no type information:
# myapp/api.py:1: error: Skipping analyzing "requests": module is installed,
# but missing library stubs or py.typed marker [import-untyped]
This message means mypy found the package but cannot verify its types. It will not infer types from an unmarked third-party package by default. The fix depends on what the library provides.
The py.typed marker
PEP 561 defines how a package declares itself type-checker-friendly. A package maintainer adds an empty file named py.typed to the package root. When mypy finds this marker, it reads the package's inline type annotations directly. Without it, mypy silently skips the package's internals and treats all values imported from it as Any.
Many widely used packages now ship py.typed directly — sqlalchemy (since 2.0), pydantic, and httpx among them. The requests library still relies on the types-requests stub package from typeshed; inline annotations are in active development but had not shipped in a stable release as of April 2026. For packages without py.typed, the next option is a separate stub package.
Stub packages and typeshed
Stub packages contain only type information — no runtime code. They follow the naming convention types-<library> and are installed separately from the library itself. For example, if you use boto3, you install boto3-stubs in your type-checking environment. mypy automatically discovers installed stub packages that follow this convention.
# Install stubs for libraries that don't ship inline types
pip install types-requests types-PyYAML boto3-stubs
# Or let mypy suggest what to install after a run
mypy --install-types myapp/
Typeshed, the official shared repository of stubs for the Python standard library and many third-party libraries, ships bundled with mypy. It handles the standard library automatically. For third-party libraries, typeshed includes stubs only for a curated set; the rest are distributed as types-* packages on PyPI.
Stub files and stubgen
When no stub package exists for a library you depend on, you can write your own .pyi stub file. A stub file has the same name as the module it covers but contains only type signatures — no implementation. mypy reads the .pyi file instead of the source when both are present.
mypy ships a bundled tool called stubgen that generates a first draft of stub files from existing Python source. The output is not production-ready — it uses Any extensively — but it creates the scaffold you then iterate on for the parts of the library you actually use.
# Generate stub drafts for an installed package
stubgen -p some_library -o stubs/
# pyproject.toml — point mypy at your stubs directory
[tool.mypy]
mypy_path = "stubs"
When first adding mypy to a project with many untyped dependencies, use ignore_missing_imports = true in a [[tool.mypy.overrides]] block scoped to the specific third-party packages causing noise, rather than setting it globally. A global setting silences errors from your own code that might also have import problems.
py.typed marker and no stub package available on PyPI. How does mypy treat values imported from that package by default?06 — suppressionSuppression: # type: ignore and When to Use It
mypy provides two inline mechanisms for suppressing errors on lines where the type system cannot accurately model what the code is doing: # type: ignore and cast(). Both are intentional tools, not workarounds — but both should be used deliberately.
# type: ignore
Adding # type: ignore at the end of a line tells mypy to suppress all errors on that line. It is the fastest way to deal with a false positive or an untyped third-party API boundary, but it provides no information about why the suppression exists or which error code it is suppressing.
The better form pins the suppression to a specific error code:
# Suppress all errors — works but opaque
result = legacy_api.fetch() # type: ignore
# Suppress only the specific error code — preferred
result = legacy_api.fetch() # type: ignore[no-any-return]
# Document the reason alongside it
result = legacy_api.fetch() # type: ignore[no-any-return] # untyped third-party API
Pinning to an error code has two benefits. First, it makes the suppression self-documenting — a reader can look up [no-any-return] and understand exactly what mypy was objecting to. Second, it allows mypy to flag the comment if the underlying error disappears (for example, after the library ships types), preventing dead suppressions from accumulating silently. Enable this behavior with warn_unused_ignores = true in your mypy configuration.
py.typed marker for a third-party package. Install the relevant types-* stub package from PyPI to resolve this properly rather than suppressing it.Any return type that actually returns Any — typically because it calls into untyped code and returns the result.[misc] is a broad suppression that can hide distinct error categories from future mypy analysis.cast() — asserting a type to mypy
cast(T, expr) tells mypy to treat the value of expr as type T, regardless of what it would otherwise infer. Unlike # type: ignore, a cast is visible in the type flow — downstream code will see the cast type, not Any. At runtime, cast() is a no-op; it returns its second argument unchanged.
import os
from typing import cast
raw = os.environ.get("PORT") # mypy infers: str | None
port = cast(int, raw) # assert to mypy: this is int
# runtime: cast() returns raw unchanged
Both # type: ignore and cast() are lies you tell mypy. They are appropriate when the type system genuinely cannot model what you are doing — interfacing with a highly dynamic API, working around a known mypy limitation, or bridging untyped code. Using them to silence errors you do not understand is a different matter: the error usually points to a real contract violation that will surface at runtime.
07 — analysisType Narrowing and Control Flow Analysis
Type narrowing is where mypy's static analysis does something that feels almost like runtime reasoning. When a variable has a union type — for example str | int — mypy tracks how conditional branches constrain the possible types within each branch, and adjusts the type it reports accordingly.
def process(value: str | int) -> str:
if isinstance(value, int):
# mypy knows: value is int here
return str(value * 2)
# mypy knows: value is str here
return value.upper()
mypy narrows types through several mechanisms: isinstance() checks, issubclass() checks, truthiness checks on Optional values, equality comparisons with Literal types, and user-defined type guard functions. PEP 742, finalized in Python 3.13, introduced TypeIs as a more precise alternative to the older TypeGuard — where TypeGuard only narrowed in the positive branch, TypeIs narrows in both the positive and negative branches of a conditional, and mypy 1.x fully supports it.
The mypy documentation is careful to note that narrowing is not strict in the sense of being foolproof — a developer can always override it with cast() or Any. But for code that relies on standard Python idioms, the control flow analysis is precise and reliable.
str | int. Inside an if isinstance(value, int): block, what type does mypy assign to value?08 — debuggingDebugging mypy: reveal_type, dmypy, and --warn-unreachable
Three tools that many developers overlook explain why mypy is behaving the way it is — and how to work with it efficiently on large codebases.
reveal_type()
When mypy infers a type that does not match your expectation, the fastest diagnostic is reveal_type(). You insert it like a function call; mypy intercepts it at analysis time and prints the inferred type as a note, without ever executing the code. Python 3.11 and later also include reveal_type() in the standard library so it no longer requires an import, but it has always worked as a mypy-specific built-in before that.
items = [1, 2, 3]
reveal_type(items)
# note: Revealed type is "builtins.list[builtins.int]"
def first(xs: list[int]) -> int:
return xs[0]
reveal_type(first)
# note: Revealed type is "def (xs: builtins.list[builtins.int]) -> builtins.int"
This is the correct tool to reach for before filing a mypy bug or before adding a cast() — it tells you exactly what mypy thinks it knows, at that exact point in the code.
dmypy — the mypy daemon
On large codebases, a full mypy run over the entire project can take several seconds or longer. The mypy daemon (dmypy) keeps a persistent server process in the background that caches the analysis state and only re-checks files that have changed. The speed difference in incremental mode is substantial — the daemon typically responds in under a second where a cold run would take 10–30 seconds on a large project.
# Start the daemon (first run is a full check)
dmypy run -- myapp/
# Subsequent runs only re-check changed files
dmypy run -- myapp/
# Stop the daemon when done
dmypy stop
One important caveat: dmypy implicitly enables --local-partial-types, which affects how partial type assignments are handled. In mypy 1.20, significant work went into reducing the behavioral divergence between daemon and non-daemon modes, and --local-partial-types will become the default in mypy 2.0, making the two modes fully consistent.
--warn-unreachable and the reworked narrowing engine
mypy 1.20's reworked narrowing engine is more aggressive than previous versions. In practice, this means mypy will now detect more code as provably unreachable — particularly after equality checks, containment tests, and match statements. This is almost always correct, but it can produce new false-positives in cases where mypy does not model non-local side effects (such as a callback modifying a variable that mypy has already narrowed).
The --warn-unreachable flag makes these determinations visible, letting you audit them rather than discover them silently. When a narrowing assumption turns out to be wrong, you can reset it with a trivial reassignment: var = var resets all narrowing for var and its attributes, as documented in the mypy 1.20 release notes.
Add warn_unreachable = true to your [tool.mypy] config after upgrading to mypy 1.20. The new narrowing engine may flag branches that were silently skipped before. Review each one — they are often genuine bugs the previous version was missing.
09 — advancedAdvanced Constructs: Protocols, Generics, and TypedDict
Beyond basic annotations, mypy supports a set of constructs that cover the harder parts of Python's type landscape.
Protocols bring structural subtyping into mypy's nominally typed system. A Protocol class defines a set of methods and attributes. Any class that implements those members satisfies the protocol, without needing to explicitly inherit from it. This is how mypy handles Python's duck typing patterns without requiring every callable or iterable to share a common base class.
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("drawing circle")
def render(shape: Drawable) -> None:
shape.draw()
render(Circle()) # mypy accepts this: Circle satisfies Drawable structurally
Generics allow you to write functions and classes that work across types while preserving type information. A container typed with TypeVar tells mypy that whatever type goes in is also the type that comes out, enabling precise checking without hardcoding a specific type.
TypedDict brings type safety to plain Python dictionaries that follow a fixed schema. Where a bare dict[str, Any] provides no guarantees about which keys exist or what types their values hold, a TypedDict definition gives mypy enough information to flag access to nonexistent keys and type mismatches in values. For structured data classes that go beyond dictionaries, see the companion Python @dataclass article.
from typing import TypedDict
class UserRecord(TypedDict):
id: int
username: str
is_active: bool
def deactivate(user: UserRecord) -> UserRecord:
return {**user, "is_active": False}
# mypy catches this:
bad: UserRecord = {"id": 1, "username": "ada", "role": "admin"}
# error: Extra key "role" for TypedDict "UserRecord" [typeddict-unknown-key]
cast(int, raw) where mypy infers raw: str | None. What does cast() actually do at runtime?10 — landscapemypy and the Broader Type Checker Landscape
mypy is the reference implementation for Python's type system and the tool most closely aligned with the PEP process — many typing PEPs were developed with mypy's behavior as the specification baseline. It is not, however, the only option, and understanding the architectural differences between the available tools matters more than their surface-level comparisons suggest.
Evaluation architecture: how each tool processes a codebase
The most consequential difference between mypy and its alternatives is not speed — it is evaluation strategy. mypy uses a multi-pass, file-at-a-time approach: it resolves imports globally, builds a full symbol table, and then walks the AST performing type inference and checking in sequential phases. This design produces highly consistent output across runs and makes incremental caching (via the dmypy daemon) predictable, but it means the first cold run over a large project is slow by design.
Pyright (Microsoft) inverts this model with lazy, demand-driven evaluation. Rather than analyzing every file upfront, pyright evaluates types on demand as they are needed by the expression being checked. This allows it to return results for a single file or a single hover request without completing a full project analysis — which is why it works so well as a language server embedded in VS Code. The practical difference: pyright's per-file feedback is nearly instant in an editor; mypy's per-file feedback requires daemon warm-up or a full project run. For batch CI checking where the entire project is always in scope, the gap narrows.
ty (Astral) takes the Rust systems-programming approach: the entire parsing, import resolution, and type inference pipeline is implemented in native code. This eliminates the Python interpreter overhead entirely. On codebases where mypy or pyright are bottlenecked by CPU time rather than I/O, the performance difference is significant. The architectural constraint is that ty has no daemon mode — it does not need one, because cold runs are fast enough that incremental caching is less critical. As of 2025, ty is in active development and its conformance coverage of the Python Typing Specification is still incomplete relative to mypy and pyright.
Unannotated code: different default behaviors matter in practice
The tools disagree on how aggressively to check unannotated code by default, and this disagreement has real consequences for legacy Python projects.
mypy's default is permissive: unannotated function bodies are largely skipped unless check_untyped_defs = true is set. This makes gradual adoption easier but means a project that never enables that flag can have deep logic errors that mypy silently ignores. Pyright checks unannotated functions by default using inferred types — it is more aggressive out of the box, which produces more errors initially but catches more bugs sooner in unannotated code. ty follows a similar aggressive-by-default approach. For teams inheriting a large unannotated codebase, this distinction determines how noisy the initial mypy run is and what annotation debt is actually visible before any configuration change.
Type narrowing conformance gaps
The tools implement different subsets of the narrowing behaviors defined in the Python Typing Specification, and their coverage of edge cases diverges in ways that matter for code that uses advanced typing patterns. mypy 1.20's reworked narrowing engine closed several gaps — particularly around equality narrowing, match statement arms, and containment expressions — but pyright and ty still handle certain patterns differently. A concrete example is narrowing through callbacks and higher-order functions: mypy does not model non-local side effects within narrowed branches (a function called inside a narrowed block might modify the narrowed variable, which mypy cannot detect), while pyright makes the same conservative assumption. ty, being newer, is still working through conformance on these edge cases.
The practical consequence: # type: ignore comments accumulated against mypy's behavior may trigger errors when running pyright, and vice versa. Migrating between checkers requires auditing every suppression comment in the codebase against the new tool's behavior.
Language server vs. batch checker: a structural distinction
mypy was designed as a batch command-line tool first; its language server protocol support is a secondary capability added later. Pyright was designed as a language server first; its CLI batch mode is a secondary interface. ty is designed to serve both roles equally from the start. This matters for teams deciding where to use each tool. Running mypy in CI and pyright in the editor is not redundant — it is a recognized pattern where the tools play to their architectural strengths. The disagreements they surface in CI become opportunities to catch cases where the typing specification is ambiguous rather than failures that must always be resolved.
Stub coverage and ecosystem integration
mypy has the broadest integration with typeshed and the widest range of types-* stub packages on PyPI, because it has been the reference implementation for the longest time. Pyright bundles a snapshot of typeshed as well, but its primary advantage is tight integration with the VS Code ecosystem and the Microsoft-maintained pyright stub overrides for libraries where typeshed's coverage is incomplete. ty integrates with Astral's toolchain — if a project already uses uv for dependency management and Ruff for linting, ty slots in with minimal configuration, sharing the same workspace model and configuration file.
Pyre (Meta) and Pytype (Google) exist for specialized internal use cases. Pyre targets large monorepo-scale codebases with its incremental Pysa security analysis layer; pytype performs type inference without annotations at all. Neither has significant adoption outside their originating organizations, and both require substantial operational investment to run effectively.
dmypy daemon for fast incremental re-checks.check_untyped_defs = true is configured.types-* stub coverage; reference PEP behavior.uv + Ruff); very large codebases where type checker throughput is the bottleneck.Choosing between these tools is a question of where in the development cycle type errors should be caught, how aggressive the default behavior should be against unannotated code, and what toolchain integration matters. mypy and pyright are not mutually exclusive — running both in CI and treating their disagreements as specification discussion points rather than failures is an established pattern in mature Python projects. A project that has invested heavily in mypy-tuned # type: ignore comments should weigh migration cost carefully before switching, since those suppressions are checker-specific and will not transfer cleanly.
11 — changelogWhat Changed in mypy 1.16 Through 1.20
mypy shipped five significant releases between May 2025 and March 2026, each advancing both functionality and performance. The release notes for all versions are published on the mypy blog and in the official changelog on Read the Docs.
mypy 1.16 (May 29, 2025) introduced support for asymmetric property types — a getter and setter can now declare different types, which mirrors common Python patterns where a setter accepts a wider range of inputs than a getter returns. It also added the experimental --allow-redefinition-new flag, which allows unannotated variables to be redefined with different types and infers union types from multiple assignments. The mypy team stated this flag is planned to become the default behavior in mypy 2.0. This release also added --exclude-gitignore to exclude paths listed in a project's .gitignore from the mypy build.
mypy 1.17 (July 14, 2025) added an --enable-error-code exhaustive-match option that flags match statements which do not handle all possible cases of an enum or union — a meaningful safety improvement for code using Python 3.10+ structural pattern matching. Support for targeting Python 3.8 was fully removed, because typeshed stopped supporting Python 3.8 after it reached end of life in October 2024. mypy itself could still run on Python 3.9 at this point; the requirement to run mypy on Python 3.10 or higher came later with mypy 1.20. This release also deprecated the --force-uppercase-builtins flag, making it a no-op.
mypy 1.18 / 1.18.1 (September 2025) was primarily a performance release. The significant improvements shipped in the 1.18.1 patch: according to the official mypy 1.18.1 release notes, the update delivered approximately a 40% speedup compared to mypy 1.17 when type checking mypy itself, with improvements of 10x or higher possible in extreme cases. The release series also introduced a new binary fixed-format cache as an experimental feature. Unlike the previous JSON-based cache, this binary format is faster to read and write and uses less disk space. The new format can be enabled with --fixed-format-cache. mypy 1.18.1 additionally added support for PEP 800 disjoint bases, introducing the @disjoint_base decorator: mypy now recognizes when two classes have mutually incompatible base classes, rejects class definitions that combine them, and uses that knowledge to improve reachability analysis and type narrowing.
mypy 1.19 (November 28, 2025) promoted the fixed-format cache from experimental to stable, noting it is planned to become the default in a future release. The release notes describe it as the last version to support Python 3.9, which reached end of life in October 2025. It also began experimental support for TypeForm[T] from PEP 747, which allows typing of expressions that represent type forms rather than values — disabled by default and requiring --enable-incomplete-feature=TypeForm. This release also introduced a new standalone error code untyped-decorator that fires when an untyped decorator silently strips a typed function's signature — a case previously impossible to detect without noisy false positives. A patch release, 1.19.1, followed on December 15, 2025.
mypy 1.20 (March 31, 2026) is the current stable release. It dropped support for running mypy with Python 3.9, which has reached end of life. Targeting Python 3.9 via --python-version 3.9 is still possible but will be removed in the first half of 2026. The headline change in this release is a substantially reworked narrowing engine: according to the mypy 1.20 release announcement, "Mypy will now narrow more aggressively, more consistently, and more correctly." The improvements affect equality expressions (==), containment expressions (in), match statements, and additional type guard expressions. The release also added support for Python 3.14 t-strings (PEP 750) and expanded mypyc-accelerated wheel availability to ARM Windows and free-threaded Python 3.14 builds.
Two infrastructure changes shipped with 1.20 that affect everyday mypy workflows. First, the binary fixed-format cache — introduced experimentally in 1.18 and stabilized in 1.19 — is now the default cache format. The legacy JSON cache is still available via --no-fixed-format-cache but will be removed in a future release. Second, an experimental Ruff-based native parser is available via pip install mypy[native-parser] and activated with the --native-parser flag. The release notes describe it as "more efficient than the default parser" and note it provides access to all Python syntax independently of the Python version used to run mypy — a benefit when checking Python 3.14 features from an older interpreter. The parser is not yet feature-complete and has known issues, but it previews the direction for mypy 2.0.
mypy 1.20 also previews several changes that will become defaults in mypy 2.0, the next planned major release: --local-partial-types will be enabled by default (making daemon and non-daemon behavior consistent), --strict-bytes will be enabled by default, and --allow-redefinition-new will be renamed to --allow-redefinition.
mypy 1.20 requires Python 3.10 or higher to run. Python 3.9 support was dropped entirely. If your project still targets Python 3.9, you can continue using --python-version 3.9 for now, but that targeting option will be removed in the first half of 2026. Plan accordingly before upgrading to mypy 1.20 or later.
12 — setupHow to Set Up mypy in a Python Project
The following steps cover the practical sequence for integrating mypy into an existing or new Python project — from installation through CI. Each step builds on the previous one and corresponds to a section covered in depth earlier in this article.
Run pip install mypy inside your project's active virtual environment. pip will install the mypyc-compiled binary wheel automatically on supported platforms (Linux x86-64/ARM64, macOS x86-64/ARM64, Windows ARM64). Pin the version in your requirements files so CI behaves consistently.
Run mypy myapp/ to check an entire package, or mypy mymodule.py for a single file. mypy will parse source files, resolve imports, infer types, and report every mismatch it finds. On the first run against an unannotated project, expect noise — that is normal and expected.
Add [tool.mypy] to your pyproject.toml. Set python_version to your target Python version. Use [[tool.mypy.overrides]] blocks to silence errors from untyped third-party packages (ignore_missing_imports = true) and to relax rules on legacy modules still being migrated.
When mypy emits [import-untyped] for a dependency, check PyPI for a corresponding types-* stub package and install it (e.g., pip install types-requests types-PyYAML). If no stub package exists, use stubgen -p libraryname -o stubs/ to generate a draft .pyi file, then point mypy at the stubs directory with mypy_path = "stubs" in your config.
Once your most important modules are annotated, add strict = true under [tool.mypy]. If strict mode produces too many errors at once, enable individual flags one at a time — start with disallow_untyped_defs = true, fix the errors it surfaces, then move to warn_return_any = true, and so on. Use per-module overrides to shield legacy modules from flags they are not ready for.
Start the mypy daemon with dmypy run -- myapp/. The first run is a full check; every subsequent run only re-analyzes changed files and responds in under a second. Stop the daemon with dmypy stop when you are done. Note that dmypy enables --local-partial-types implicitly, which will become the mypy default in version 2.0.
Add a mypy step after linting and before the test suite. As of mypy 1.20, the binary fixed-format cache is now the default — no extra flag is needed. If upgrading to mypy 1.20, add warn_unreachable = true to your config and review the new unreachable-code findings — they are frequently real bugs the previous narrowing engine missed. Use --no-fixed-format-cache only if you have tooling that still parses the legacy JSON cache format.
13 — faqFrequently Asked Questions
These are the questions that come up consistently when developers first add mypy to a project or upgrade to a recent release.
What does mypy do to my Python code?
mypy parses your source files into an abstract syntax tree, resolves imports, performs type inference, and checks annotated types against one another — all without executing your code. It reports type mismatches as errors. Your program runs exactly the same whether or not mypy reports errors, because Python ignores type annotations at runtime.
Does running mypy slow down my Python program?
No. mypy is a static analysis tool that runs separately from your program. Type annotations are metadata — the interpreter treats them as ignored at runtime. Running mypy has zero effect on your program's execution speed. mypyc is a separate tool that compiles Python to C extensions for actual runtime speedup; that is distinct from the type-checking function of mypy.
What is the latest stable version of mypy?
As of April 2026, the latest stable release is mypy 1.20.0, published March 31, 2026. It requires Python 3.10 or higher to run. Key changes include a reworked narrowing engine, the binary fixed-format cache now being the default, and an experimental Ruff-based native parser available via pip install mypy[native-parser]. See the changelog section above for a full account of what changed across the 1.16 through 1.20 series.
Why does mypy not report errors in some of my functions?
By default, mypy skips type-checking the bodies of functions that have no type annotations. It will flag call sites where a type mismatch occurs, but it will not analyze the internals of an unannotated function. Enable check_untyped_defs = true in your configuration to change this behavior, or use --strict to enable the full set of stricter checks including disallow_untyped_defs.
What does mypy do when a third-party library has no type information?
mypy will not analyze a third-party package unless it provides a py.typed marker file (PEP 561) or a corresponding stub package is installed. Without either, imports from that library are treated as Any, and mypy emits an [import-untyped] warning. The practical fix is to install the relevant types-* stub package from PyPI, or to use ignore_missing_imports = true as a per-module override for libraries where no stubs exist.
When should I use # type: ignore versus cast()?
Use # type: ignore[error-code] when mypy is flagging an error on a line that is correct at runtime but cannot be expressed in the type system — typically at a boundary with untyped third-party code. Use cast(T, expr) when you need to assert a more specific type to mypy so that downstream code benefits from the narrower type. A cast keeps the type visible to mypy's analysis; a type: ignore comment suppresses the error but leaves downstream code without the corrected type.
What is the difference between mypy and pyright?
Both are static type checkers for Python that read PEP 484-compatible annotations. Pyright is faster, particularly on large codebases, and powers VS Code's Pylance extension with real-time editor feedback. mypy has broader library stub coverage through typeshed and is the historical reference implementation for typing PEPs. The two tools occasionally disagree on edge cases in the typing specification, particularly around more advanced features. Teams sometimes run both in CI and treat the union of their findings as the ground truth.
What Python versions does mypy 1.20 support?
mypy 1.20 requires Python 3.10 or higher to run. Python 3.9 support was dropped in this release. You can still type-check code targeting Python 3.9 using --python-version 3.9, but that targeting option will be removed in the first half of 2026. mypy supports checking code targeting Python 3.10 through 3.14.
14 — summaryKey Takeaways
- mypy operates independently of the Python runtime. It parses source code into an AST and performs type inference and checking without executing anything. Errors it reports are analysis findings, not runtime failures — and your code will still run even when mypy reports errors.
- Gradual typing makes adoption realistic. You can annotate one module at a time, use
Anyas an intentional escape hatch for dynamic sections, and expand coverage incrementally. The tool is designed to meet codebases where they are, not demand a full rewrite upfront. - Third-party library types require explicit setup. mypy will not infer types from an unmarked package. The typical resolution is installing a
types-*stub package from PyPI, looking for apy.typedmarker in the library itself, or writing minimal.pyistubs usingstubgenas a starting draft. - Strict mode closes the gaps that permissive defaults leave open. Flags like
disallow_untyped_defsandwarn_return_anypreventAnyfrom propagating silently through a codebase. For production projects with meaningful annotation coverage, enabling strict mode is the standard recommendation. - Use suppression deliberately, with error codes.
# type: ignore[error-code]is the correct way to silence a known false positive — pinned to the specific error so mypy can flag the comment if the issue ever resolves.cast()is the right tool when you need to assert a type that flows forward through the analysis, not just suppress a single line. - Control flow analysis makes type narrowing precise. mypy tracks how
isinstance()checks, truthiness tests, and user-defined type guards constrain types within conditional branches, allowing it to reason about union types the way a developer would reason at runtime. - Three tools resolve almost every mypy confusion.
reveal_type()prints exactly what mypy infers at any point in the code.dmypykeeps a daemon server for incremental re-checks that run in under a second.--warn-unreachablesurfaces the code paths the narrowing engine has determined can never execute. - mypy is not the only type checker, and the choice matters. Pyright is faster and offers tighter IDE integration; ty offers extreme performance for very large codebases. The tools occasionally disagree on typing edge cases. Teams migrating from one to another should audit existing
# type: ignorecomments, as they are tuned to one checker's specific error behavior. - Recent releases prioritize performance and stricter defaults. The binary fixed-format cache — now the default as of mypy 1.20 — delivered approximately 40% faster incremental builds in typical cases (10x+ in extreme cases) compared to the legacy JSON format. mypy 1.20 also introduced a substantially reworked narrowing engine and an experimental Ruff-based native parser. The trajectory is clearly toward mypy 2.0, where behaviors like
--local-partial-typesand--strict-byteswill become defaults.
mypy does not transform Python into a statically typed language. It adds a verifiable layer of documentation on top of a language that remains dynamically typed at its core. That distinction matters: the goal is not to constrain what Python can do, but to make the contracts between parts of a program explicit and machine-checkable. For any codebase that has grown beyond the point where a single developer can hold all of its interfaces in their head at once, that layer of verification pays for itself quickly.