What Is mypy? Python's Static Type Checker Explained

Python lets you run code that is completely wrong about types — and it won't tell you until the moment something breaks at runtime, possibly in production, possibly at 2 a.m. Mypy is the tool that finds those problems before you ever execute a single line. It is an optional static type checker that reads your type annotations and reports errors the Python interpreter never would.

How to install and use mypy: quick-start steps

For readers who want to get running immediately, here is the condensed sequence before the detailed explanations that follow.

  1. Install mypy — Run pip install mypy inside your project's virtual environment. Mypy 1.19.1 requires Python 3.9 or later.
  2. Run a type check — Point mypy at a file or package: mypy program.py or mypy my_package/. It will print errors in the format filename:line: error: message [error-code], or report clean.
  3. Read the output — Each error includes a filename, line number, plain-English message, and a square-bracketed error code like [arg-type]. The error code tells you the exact rule and lets you target a suppression precisely.
  4. Configure in pyproject.toml — Add a [tool.mypy] section to set python_version, warn_return_any = true, and warn_unused_ignores = true. Graduate to disallow_untyped_defs = true or full strict = true as your annotations mature.
  5. Suppress precisely when needed — Use # type: ignore[error-code] rather than bare # type: ignore. Per-module ignore_errors = true overrides in pyproject.toml are the right escape hatch for legacy modules.
  6. Add to CI — Drop mypy . into a GitHub Actions step. For large codebases, switch to dmypy run -- . for daemon-mode incremental speed. For pre-commit, use the mirrors-mypy hook.

The sections below cover each of these steps in full detail, including configuration options, type system features, editor integration, and the tools that automate annotation generation on large existing codebases.

Mypy is an optional static type checker for Python. You run it as a separate command-line tool — not as part of the Python interpreter — and it analyzes your source files for type errors without executing any of your code.

The word "optional" is load-bearing here. Python has always been a dynamically typed language, meaning the interpreter figures out types at runtime and does not enforce them at all. Mypy does not change that. Your program still runs exactly the same way whether mypy approves of it or not. What mypy gives you is a second opinion, delivered before execution, that catches the kinds of bugs Python will silently let through until they explode.

Consider a trivially broken function:

user_input = input("Enter a number: ")
print(user_input + 1)  # This crashes at runtime with TypeError

Python does not warn you about this when you write it. It waits until someone actually runs that line. Mypy, on the other hand, catches it immediately:

# error: Unsupported operand types for + ("str" and "int")

This is the core value proposition: bugs caught at check time cost far less to fix than bugs caught at run time, especially in production systems or large codebases where type mismatches can propagate silently through many layers before something finally blows up.

Note

Mypy type hints follow PEP 484, which was added to the language in Python 3.5. The annotations are part of standard Python syntax and are completely ignored by CPython at runtime. Adding them does not slow your program down.

Origin: from a PhD thesis to a Python standard

Jukka Lehtosalo began work on mypy in 2012 while he was a PhD student at the University of Cambridge Computer Laboratory. The project borrowed heavily from his earlier work on a language called Alore, which was a Python-inspired language with an optional static type system built in from the start.

The original idea was actually more radical: mypy was conceived as a separate Python-like language that had static types. The pivot to being an add-on checker for standard Python happened when Lehtosalo met Guido van Rossum — Python's creator — at PyCon 2013. They agreed that the tool would be far more useful if it worked with existing Python programs rather than requiring developers to switch to a different language.

At the Python Language Summit in 2013, van Rossum and Lehtosalo agreed that layering mypy onto standard Python — rather than building a new language — was the only path to broad adoption. That decision defined everything that followed. (Source: LWN.net)

That meeting had lasting consequences. The two collaborated on what became PEP 484, co-written by van Rossum, Lehtosalo, and Łukasz Langa, which shipped with Python 3.5 in 2015 and formally standardized the type hint syntax. Mypy served as the reference implementation of that PEP — meaning the way mypy interpreted annotations became the definition of how they were supposed to work.

The industrial proof of concept came from Dropbox, where both van Rossum and Lehtosalo worked. Dropbox built one of the largest Python codebases in the world — primarily for its backend services and desktop client — and adopted mypy at scale. By 2019, Dropbox had annotated nearly four million lines of Python with mypy, a milestone that demonstrated static typing was feasible even on an enormous legacy codebase that was never designed for it.

At scale, Dropbox engineers concluded that dynamic typing was making their Python codebase harder to understand and was measurably slowing the team down. (Source: Dropbox Engineering Blog)

Today mypy is maintained under the python organization on GitHub and continues to receive regular releases. The most recent stable version at the time of writing is mypy 1.19.1 (December 2025), and active development is building toward 1.20, with alpha releases already on PyPI as of early 2026. Mypy 1.19 was the last release to support Python 3.9, which reached end-of-life in October 2025 — meaning mypy 1.20 will require Python 3.10 or later at minimum. The project is also planning a mypy 2.0 release, with the core team noting in a February 2026 planning discussion that they intend to limit breaking changes to only those that are strictly necessary or have minor fallout, to avoid a situation like the Python 2-to-3 migration.

How mypy works

At a conceptual level, mypy does three things: it reads your source files and the type annotations in them, it builds a model of what types every variable and expression should be, and it reports anywhere that model has a contradiction.

Under the hood, mypy parses your Python source into an abstract syntax tree, resolves imports to gather type information from stub files (.pyi files) and inline annotations, and then performs a constraint-solving pass that propagates type information through the program. Where two types are incompatible — passing a str to a parameter annotated as int, or calling a method that does not exist on a type — mypy emits an error.

Mypy uses bidirectional type inference, which means it can often figure out types you have not explicitly annotated. If you write x = 5, mypy infers that x has type int without you needing to write x: int = 5. This makes it practical to get useful checking even in code that is only partially annotated — a critical feature for migrating existing projects.

Pro Tip

Mypy's reveal_type() function is a built-in diagnostic. Put reveal_type(some_variable) anywhere in your code and mypy will print exactly what type it inferred for that expression. It is one of the most useful tools for understanding what mypy sees in complex code. On Python 3.11 and later, reveal_type is available from the typing module and works at runtime too — import it with from typing import reveal_type and it will print the runtime type to stderr. On older Python versions (or without importing it), it is mypy-only and will raise a NameError at runtime, so remove it before running your program.

Installing and running mypy

Mypy installs via pip and requires Python 3.9 or later to run the tool itself — note that mypy 1.19 is the last release that supports Python 3.9, and mypy 1.20 will require Python 3.10 or later. The code you are checking can still target older Python versions using --python-version:

pip install mypy

Once installed, point it at any Python file or package:

# Check a single file
mypy program.py

# Check an entire package
mypy my_package/

# Check with a specific Python version target
mypy --python-version 3.11 program.py

If you run mypy on an existing unannotated codebase, it will likely report very few errors — intentionally. Mypy defaults to treating unannotated code as dynamically typed (Any), which means it will not flag things it cannot verify. This is the gradual typing design: you get more checking as you add more annotations, but you are never forced to annotate everything at once.

It is worth installing mypy inside a virtual environment for each project. This keeps the version of mypy consistent with your project's requirements and avoids conflicts with other tools. The activation steps on Windows are the same as for any other package.

Reading mypy's output

Before you configure anything, it helps to understand what mypy is actually telling you when it does find a problem. The output format is consistent and deliberately terse:

program.py:6: error: Argument 1 to "greet" has incompatible type "int"; expected "str"  [arg-type]
program.py:10: error: Item "None" of "str | None" has no attribute "upper"  [union-attr]
Found 2 errors in 1 file (checked 1 source file)

Each line follows the same structure: filename:line_number: severity: message [error-code]. The filename and line number tell you exactly where to look. The severity is almost always error, though mypy can also emit note lines that provide context for a preceding error. The message is a plain-English description of the type contradiction. The square-bracketed label at the end — [arg-type], [union-attr], [assignment], and so on — is the error code.

Error codes matter for two reasons. First, they let you look up the exact rule mypy is enforcing in the documentation. Second, they let you suppress a specific error without silencing everything on that line. A full list of error codes is maintained in the mypy documentation. The ones you will encounter most often are [arg-type] (wrong argument type), [return-value] (return type mismatch), [assignment] (incompatible assignment), [attr-defined] (attribute does not exist on the type), and [name-defined] (name is not defined or not imported).

The final summary line — Found N errors in M files (checked K source files) — is useful in CI: a zero-error run exits with code 0, so mypy integrates naturally into any pipeline that treats a non-zero exit code as a failure.

Configuration and strictness levels

Mypy is configured through a mypy.ini file, a setup.cfg section, or — the modern standard — a [tool.mypy] section in pyproject.toml. Configuration lets you dial in exactly how strict you want the checking to be.

# pyproject.toml
[tool.mypy]
python_version = "3.12"
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true

The most important flag to understand is --strict. This single flag enables a bundle of the most rigorous checks, including requiring all functions to have full type annotations, forbidding implicit Any, and warning on return type Any. For new projects, starting with --strict is reasonable. For existing codebases, it is often too aggressive to enable all at once — the recommended approach is to turn on individual checks gradually as you annotate more of the code.

Mypy configuration flags: what each enforces and whether it is included in --strict mode
Flag What it enforces Included in --strict?
--disallow-untyped-defs Error if any function lacks full type annotations Yes
--disallow-any-generics Forbid bare generic types like List without a parameter Yes
--warn-return-any Warn when a typed function returns Any Yes
--check-untyped-defs Check the body of unannotated functions Yes
--ignore-missing-imports Silence errors for third-party libraries without stubs No
--no-implicit-optional Require explicit Optional[X] rather than inferring it from None defaults Yes

You can also configure mypy on a per-module basis. This is useful when migrating a large codebase: you can apply strict checking to new modules while allowing older, unannotated modules to pass silently.

# pyproject.toml — per-module overrides
[tool.mypy]
strict = true

[[tool.mypy.overrides]]
module = "legacy_module.*"
ignore_errors = true
New in mypy 1.18 and 1.19: Fixed-Format Cache

Mypy 1.18 introduced a new binary cache format (--fixed-format-cache) that makes incremental builds up to twice as fast as the older JSON-based cache. In mypy 1.19 the feature graduated from experimental to stable, and the project plans to enable it by default in mypy 1.20. If you are on 1.19.1 and want faster CI incremental runs, you can opt in now: add fixed_format_cache = true to your [tool.mypy] configuration block. A built-in conversion tool (python -m mypy.exportjson) lets you read the binary cache files if you have tooling that parsed the old JSON format.

Suppressing errors with # type: ignore

There are situations where mypy flags something you know is fine: a third-party library that ships no stubs, a dynamic pattern mypy cannot follow, or code you are intentionally leaving untyped during a migration. The escape hatch is a comment placed at the end of the offending line:

# Silences all mypy errors on this line — avoid when possible
result = some_dynamic_call()  # type: ignore

# Preferred: silence only the specific error code that applies
result = some_dynamic_call()  # type: ignore[no-untyped-call]

The bare # type: ignore suppresses every error mypy would emit on that line, which is a blunt instrument. The targeted form — # type: ignore[error-code] — is preferable because it leaves other errors visible and makes it obvious what you were suppressing and why. If you later fix the underlying issue and the error code no longer applies, mypy will warn you that the ignore comment is now unnecessary — provided you have warn_unused_ignores = true in your configuration, which is enabled by strict mode.

Watch Out

# type: ignore comments are real technical debt. They communicate to the next developer — and to future-you — that mypy was intentionally bypassed here. Accumulating them without a comment explaining the reason creates confusion. A good convention is to always pair a suppression with a brief explanation: # type: ignore[attr-defined] # SQLAlchemy column access not yet stubbed. This costs one line and saves significant future confusion.

For entire files you want mypy to skip, the exclude option in pyproject.toml is cleaner than sprinkling ignores everywhere. For individual modules in a large codebase, the per-module ignore_errors = true override shown in the configuration section above is the right tool.

Spot the Bug

One line in this function will cause mypy to report an error. Which line is it?

Click on the line you think is the problem, then check your answer.

click a line to select it

Bug found

The type system: what mypy can check

Mypy supports the full range of Python type annotations defined in typing, collections.abc, and the newer built-in generics available since Python 3.9 and 3.10. Working through python tutorials will give you plenty of practice with the annotation syntax before you bring mypy into the picture, and the guide to type annotations for readability and IDE support covers the practical notation in depth. Here is a sampling of what mypy understands:

  • Basic typesint, str, float, bool, bytes, None
  • Genericslist[int], dict[str, Any], tuple[int, str, float]
  • Union typesstr | int (Python 3.10+) or Union[str, int]
  • OptionalOptional[str], equivalent to str | None
  • CallableCallable[[int, str], bool] for function signatures
  • TypedDict — typed dictionaries with specific key-value pairs
  • Protocols — structural subtyping (duck typing made statically verifiable)
  • TypeVar and Generics — parameterized types for generic functions and classes
  • Literal typesLiteral["GET", "POST"] for constrained string values
  • Final — values that should not be reassigned

Protocols deserve special attention because they capture something unique to Python. A Protocol defines a structural interface: any class that has the required methods and attributes satisfies the protocol, without needing to explicitly inherit from it. This is how Python duck typing works at the language level, and mypy can verify it statically.

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None: ...

class Circle:
    def draw(self) -> None:
        print("Drawing a circle")

class Square:
    def draw(self) -> None:
        print("Drawing a square")

def render(shape: Drawable) -> None:
    shape.draw()

# Mypy accepts both — neither inherits from Drawable
render(Circle())
render(Square())

Mypy also understands type narrowing — the way Python's control flow constrains types. An isinstance() check inside an if block causes mypy to treat the variable as the narrower type within that branch, exactly the way a human reader would.

For popular third-party libraries that do not ship their own type annotations, mypy uses stub files.pyi files that describe the public API of a module without any implementation. The typeshed repository maintains stubs for the Python standard library and many common packages. Additional stubs are available as separate packages on PyPI, typically named types-<package>.

Editor integration and CI/CD

Mypy is useful on the command line, but the workflow improves considerably once it is running automatically — either in your editor as you type, or in a pipeline that blocks merges on type errors.

Editor integration

For VS Code, the Mypy Type Checker extension (published by Microsoft) runs mypy in the background and surfaces errors inline as you edit. Install it from the VS Code marketplace and it will pick up your project's pyproject.toml configuration automatically. Note that VS Code also ships the Pylance extension, which uses Pyright under the hood — if you want mypy specifically rather than Pyright, install the dedicated mypy extension and ensure Pylance's type checking mode is not conflicting with it.

For PyCharm, mypy is available as a third-party plugin via the JetBrains Plugin Marketplace. PyCharm also has its own built-in type inference engine, so running both simultaneously will produce two parallel sets of warnings — which is fine, as the two systems sometimes catch different things, but can feel noisy until you tune the settings.

For editors that support the Language Server Protocol, mypy-lsp and the broader python-lsp-server ecosystem offer paths to mypy integration in Neovim, Emacs, Helix, and others. The setup varies by editor, but the underlying mechanism is the same: the editor invokes mypy, parses the output, and maps errors back to source positions.

Running mypy in CI/CD

Because mypy exits with code 1 when it finds errors and code 0 when it does not, adding it to a CI pipeline is straightforward. A minimal GitHub Actions step looks like this:

# .github/workflows/type-check.yml
name: Type Check

on: [push, pull_request]

jobs:
  mypy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install mypy
      - run: mypy .

For projects with many dependencies, install the project's full requirements (or at minimum the types-* stub packages for third-party libraries) before running mypy, so it has the information it needs to check calls into those libraries.

For pre-commit hooks, the mirrors-mypy hook is the standard approach. Add it to your .pre-commit-config.yaml and mypy will run against staged files before every commit:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.19.1
    hooks:
      - id: mypy
        additional_dependencies: [types-requests, types-PyYAML]
Pro Tip

On large codebases in CI, enable daemon mode by calling dmypy run -- . instead of mypy .. The daemon caches its full program state between runs, so incremental checks on changed files are dramatically faster. Pair this with the fixed_format_cache = true option from mypy 1.19 and you can cut CI type-checking time by 50% or more on codebases with hundreds of modules.

Mypy vs. Pyright, Pytype, and Pyre

Mypy is not the only Python type checker. Understanding the landscape helps you choose the right tool and know what you are getting.

Comparison of Python static type checkers: mypy, Pyright, Pytype, and Pyre
Tool Made by Key strengths Notable tradeoffs
mypy Open source / Dropbox Longest track record, plugin system, mypyc compilation, broadest community stubs Slower on very large codebases than Pyright without daemon mode
Pyright / Pylance Microsoft Very fast, excellent VS Code integration via Pylance, stricter defaults No plugin system; some edge cases diverge from mypy's interpretation of PEP 484
Pytype Google Can infer types without annotations; useful for legacy code Slower, less widely adopted, harder to set up outside Google's ecosystem
Pyre Meta Extremely fast, designed for massive monorepos Complex setup, built primarily for Meta's internal toolchain

For teams starting fresh, Pyright is often recommended for VS Code-centric workflows because Pylance integrates it directly into the editor. Mypy remains the default choice for CI pipelines, pre-commit hooks, and projects that need the plugin ecosystem — for example, mypy plugins exist for SQLAlchemy, Django, Pydantic, and other frameworks that use dynamic patterns PEP 484 cannot directly describe.

Gradual typing: you don't have to annotate everything

One of the design principles Lehtosalo and van Rossum built into both mypy and PEP 484 is gradual typing: you can start with no annotations and add them incrementally. Unannotated code is treated as dynamically typed. Mypy will check what it can verify and stay silent about the rest.

This matters enormously in practice. It means you do not face a binary choice between "fully annotated codebase" and "no type checking at all." You can annotate the most critical functions first — the ones that are called in many places, or the ones that handle user input and boundary data — and get immediate value from mypy without a massive up-front investment.

The Any type is the escape hatch. Any value typed as Any is compatible with every other type in both directions. Mypy does not check operations on Any values. When you call a function that returns Any, the result can be used as though it were any type. This is intentional: it lets unannotated code coexist peacefully with annotated code.

Watch Out

Any is a hole in the type system. A value typed as Any will silently pass mypy's checks regardless of how you use it, which can give a false sense of safety. If you see a lot of Any in your codebase — especially as return types from external libraries — be aware that mypy is not checking those paths. The --warn-return-any flag helps surface this.

Practical solutions when mypy adoption is hard

The commonly cited advice — "annotate gradually," "use --ignore-missing-imports," "suppress what you can't fix" — gets teams started but often stalls mid-migration. These are the solutions that actually move the needle when the surface-level approach stops working.

Seed annotations automatically before touching any code by hand

Manually annotating a legacy codebase is the main reason mypy adoption stalls. The practical entry point is to use a runtime trace tool to generate annotations automatically. MonkeyType, developed at Instagram, runs your test suite or application with tracing enabled and records the actual types that flow through every call. It then generates .pyi stub files — or applies annotations inline — based on what it observed. The annotations are not always production-quality (they may over-specify, using concrete types where a base class would be more appropriate), but they give mypy enough to check against immediately and give your team a concrete starting point rather than a blank canvas.

A complementary tool is autotyping, which uses static heuristics to infer simple, high-confidence annotations — return types like None, bool, and str on trivially typed functions — without requiring a running application. Using MonkeyType for dynamic inference and autotyping for static heuristics together can annotate 30–60% of a codebase before you write a single annotation by hand.

Use reveal_type() as a debugging discipline, not a one-off diagnostic

Mypy's built-in reveal_type() is usually mentioned as a convenience tip. In practice, it is the primary tool for diagnosing why mypy is making unexpected decisions. When an error message does not make sense, or when mypy is treating a value as Any and you do not understand why, the right move is to trace backward: insert reveal_type() at each stage of the data flow to find exactly where the type information is being lost or widened. A common culprit is a call into an unannotated function that returns Any — the Any then propagates forward and silently disables checking for everything downstream. reveal_type() makes that propagation visible so you can target the right fix.

Treat Any propagation as the primary metric, not error count

Teams often measure mypy adoption by how many errors are reported or suppressed. A more useful metric is how much of your code is operating on verified types versus Any. The --warn-return-any flag surfaces places where typed functions are returning Any — usually because they call into unannotated code. The --disallow-any-generics flag catches bare generics like list or dict without type parameters, which are another source of silent Any propagation. Running mypy with the --txt-report or --html-report flag generates a coverage report that shows which modules and functions are fully typed versus partially typed versus untyped. Using that report as a team dashboard — rather than just watching error counts — gives a cleaner picture of actual progress.

Use Protocols to break dependency cycles that block annotation

A pattern that frequently blocks annotation on large codebases is circular imports: module A needs types from module B, and module B needs types from module A. The standard advice is to use TYPE_CHECKING guards, which is correct but not always sufficient when the dependency is deep. The more durable solution is to introduce a Protocol that both modules can depend on independently. Instead of importing a concrete class for type hints, define a Protocol in a shared types module that describes only the interface you actually need. Both modules depend on the Protocol, not on each other. This pattern is also useful when annotating code that interacts with frameworks through dynamic patterns — you define a Protocol for what the framework provides rather than fighting with incomplete stubs.

Run Pyright in parallel during migration to catch divergences

Mypy and Pyright agree on the vast majority of well-typed code, but they diverge on edge cases and on inference for partially annotated code. Running both checkers in parallel during a migration is not redundant: Pyright is often stricter by default and will flag things mypy's leniency misses, particularly around narrowing and inference in complex control flow. The practical workflow is to run mypy as the blocking check in CI — since it is the team's authoritative checker — and Pyright in a non-blocking advisory mode, flagging divergences as issues to investigate rather than as immediate failures. This surfaces ambiguities in your type annotations before they become assumptions baked into production code.

Manage # type: ignore debt with a structured audit, not ad-hoc cleanup

Most teams accumulate suppression comments during initial migration and then never revisit them. The right approach is a periodic audit using mypy's warn_unused_ignores = true flag combined with a grep for bare # type: ignore (without an error code). Bare ignores are the most dangerous: they suppress every error on a line, including future errors introduced by refactoring. Converting them to targeted # type: ignore[error-code] forms is mechanical but high-value maintenance. On large codebases, this can be scripted: run mypy with --show-error-codes, parse the output, and automatically append the appropriate error codes to existing bare ignore comments. Tools like mypy-upgrade automate exactly this workflow.

Configure per-module strictness as a progressive contract, not just an escape hatch

The per-module override system in pyproject.toml is typically presented as a way to silence old code while you work on it. A more powerful use is to treat it as a progressive enforcement contract: new modules added to the codebase default to strict = true, while existing modules are explicitly listed with their current permissive settings. This means every new module starts typed from day one, and existing modules have a documented debt status rather than an implicit one. The list of permissive modules in pyproject.toml then becomes an actionable backlog — you can track coverage progress by watching the list shrink over time, one module at a time, rather than managing a diffuse set of suppression comments scattered through the codebase.

Mypyc: compiling Python with mypy

There is a lesser-known sibling to mypy called mypyc. Where mypy uses type annotations to find bugs, mypyc uses the same annotations to compile Python modules to C extensions — native code that runs significantly faster than interpreted Python.

Mypyc is not a general-purpose Python compiler: it requires fully annotated code, does not support every Python construct, and produces modules that must be imported like regular Python packages. But where it applies, the speedup is substantial. Mypy itself is compiled with mypyc, which makes the type checker roughly four times faster than running it as pure Python. The mypy 1.18.1 release reported around a 40% speedup compared to 1.17 when checking mypy itself, with the team noting that in extreme cases the improvement can reach 10x or higher, driven by mypyc improvements and type-caching optimizations. As of mypy 1.19, mypyc also supports Python 3.14 and ships pre-built mypyc-compiled wheels for Windows ARM64, making the performance gains easier to access for a broader range of developers.

For developers who want to accelerate Python hot paths without rewriting them in C, Cython, or Rust, mypyc represents a third option that reuses the type annotations you are already adding.

Key takeaways

  1. Mypy is a static checker, not a runtime enforcer. It runs before your program does and reports type errors without changing how Python executes your code. Your program runs the same whether mypy finds errors or not.
  2. It was designed for gradual adoption. Unannotated code is treated as dynamically typed. You can annotate one function at a time and get more checking as you go — no big-bang migration required.
  3. Learn to read the output. Every mypy error includes a filename, line number, plain-English message, and a square-bracketed error code. Error codes let you look up the exact rule being enforced and suppress only that specific error if needed.
  4. Use targeted suppression, not blanket ignores. # type: ignore[error-code] is far preferable to bare # type: ignore. It makes your intent explicit and lets mypy continue checking other errors on the same line. Add warn_unused_ignores = true so stale suppressions get flagged automatically.
  5. Configuration is the key to practical use. The default settings are permissive. Use pyproject.toml to enable stricter flags incrementally, and use per-module overrides to insulate legacy code while enforcing stricter rules on new code.
  6. Integrate early, both locally and in CI. The VS Code Mypy extension and the mirrors-mypy pre-commit hook catch errors before they reach a pull request. In CI, mypy's zero/non-zero exit code plugs directly into any standard pipeline — add dmypy run and the fixed-format cache for large codebases to keep check times under a second.
  7. Stubs extend coverage to unannotated libraries. Typeshed and types-* packages on PyPI give mypy the information it needs to check code that uses third-party libraries.
  8. Annotation does not have to be manual. Tools like MonkeyType (runtime tracing) and autotyping (static heuristics) can seed 30–60% of a codebase's annotations automatically. Use reveal_type() systematically to trace Any propagation and target the right fixes. Treat the per-module override list as a live debt backlog, not a permanent escape.
  9. Mypy has a compiler sibling. Mypyc reuses mypy's type analysis to compile annotated Python to C extensions, making it a performance tool as well as a correctness tool.
  10. Performance has improved substantially. Mypy 1.18 introduced a binary fixed-format cache that makes incremental builds up to twice as fast. Combined with daemon mode (dmypy), large codebases can get sub-second incremental checks. Enable the new cache with fixed_format_cache = true in your config.
  11. The ecosystem has grown considerably. Alternatives like Pyright are worth knowing, but mypy's plugin system, typeshed contributions, and community maturity make it a solid default — particularly for CI-first workflows and framework-heavy projects. A mypy 2.0 is also in active planning as of early 2026.

If you are building anything beyond a short script, the question is no longer whether to use a type checker but when to start. Mypy's answer to that question has always been: now, one annotation at a time, at whatever pace makes sense for your codebase. The current stable release is mypy 1.19.1; mypy 1.20 is in alpha as of early 2026 and will drop Python 3.9 support. You can verify the current state of mypy's documentation and changelog at mypy.readthedocs.io and track upcoming releases on GitHub.

For more on how type annotations interact with Python's dynamic type system at runtime, see Python Type Hints Without Breaking Dynamic Typing. For the concrete failure modes that make type checking worthwhile in the first place, see What Happens When You Assign the Wrong Type to a Variable in Python and How Python Strong Typing Prevents Runtime Type Errors.

Knowledge check: pop quiz

Three questions to test your understanding of what you just read. Click an answer to see the feedback — use "Try again" to explore the other responses before moving on.

Pop Quiz

Frequently asked questions

What is mypy?
Mypy is an optional static type checker for Python. It reads type annotations in your source code and reports type errors before you ever run the program, without changing how Python executes the code at runtime.
Does mypy change how Python runs my code?
No. Mypy is a static analysis tool that runs separately from the Python interpreter. Your program runs exactly the same whether mypy reports errors or not. Type hints are ignored by CPython at runtime.
What Python version does mypy require?
Mypy 1.19.1 (the current stable release as of March 2026) requires Python 3.9 or later to run the tool itself. Mypy 1.19 is also the last release to support Python 3.9, which reached end-of-life in October 2025. The upcoming mypy 1.20 will require Python 3.10 or later. You can still check code targeting older Python versions using the --python-version flag.
What is the difference between mypy and Pyright?
Mypy is the original reference implementation of PEP 484, with the broadest plugin ecosystem (supporting Django, SQLAlchemy, Pydantic, and others) and the longest community track record. Pyright, made by Microsoft, is significantly faster and integrates directly into VS Code through the Pylance extension. Both follow the same PEP 484 standard but differ on edge cases and defaults. Mypy is typically favored for CI pipelines and framework-heavy projects; Pyright is often preferred for editor-first, VS Code-centric workflows.
What is mypyc?
Mypyc is a compiler that uses mypy's type annotations to compile annotated Python modules into C extensions, typically achieving a 3–5x speedup over interpreted Python. Mypy itself is compiled with mypyc, making the type checker approximately four times faster than running it as pure Python.
Do I have to annotate all my Python code before using mypy?
No. Mypy is designed for gradual typing. Unannotated code is treated as dynamically typed (Any), so you can start with zero annotations and add them incrementally. You can annotate the most critical functions first and get immediate checking value without annotating everything at once.
How do I read a mypy error message?
Mypy errors follow the format filename:line: error: message [error-code]. The filename and line number tell you where to look. The message describes the type contradiction in plain English. The square-bracketed label at the end — such as [arg-type] or [return-value] — is the error code, which you can use to look up the exact rule in the documentation or to suppress only that specific error if needed.
How do I suppress a specific mypy error?
Add a # type: ignore[error-code] comment at the end of the offending line, using the specific error code from mypy's output — for example # type: ignore[arg-type]. This silences only that error while leaving other checks active. Avoid the bare # type: ignore form, which suppresses all errors on the line regardless of cause. Enable warn_unused_ignores = true in your configuration so mypy flags stale suppression comments when the underlying issue is fixed.
How do I run mypy in CI/CD?
Mypy exits with code 0 when no errors are found and code 1 when errors are present, so it plugs directly into any pipeline that treats a non-zero exit as a failure. In GitHub Actions, add a step that installs mypy and runs mypy . after your dependencies are installed. For pre-commit hooks, the mirrors-mypy hook is the standard approach. For faster incremental runs on large codebases, use dmypy run -- . instead of mypy . to take advantage of daemon mode.
Can I annotate a large existing codebase without doing it all by hand?
Yes, and you should. Two tools handle different parts of the problem. MonkeyType runs your existing tests or application with tracing enabled and records the types that actually flow through each function call, then generates annotations from those observations. It works best on code that has good test coverage. autotyping handles the complementary case: it uses static heuristics to annotate trivially typed functions — those that return None, bool, str, and similar simple types — without needing a running application. Using both together can cover a substantial portion of a legacy codebase before you write a single annotation by hand. The generated annotations may need review for accuracy, particularly where MonkeyType observed only one concrete type where a more general base class would be more appropriate, but they give mypy something to check against immediately.