backend / python core / typing / 07_mypy_pyright_strictness.md

mypy vs pyright; strictness modes; runtime enforcement

3 min read source

mypy vs pyright; strictness modes; runtime enforcement

The two main type checkers

mypy pyright
Author Python core devs (Dropbox origin) Microsoft
Speed Slower (~10x for large repos) Very fast
IDE integration mypy daemon, plugins Pylance / VS Code native
Strictness Permissive by default Stricter by default
Inference power Less aggressive More aggressive
Configuration mypy.ini, pyproject.toml pyrightconfig.json, pyproject.toml
Plugins Yes (Django, SQLAlchemy, Pydantic) Limited

For new projects, pyright (via Pylance) is becoming the default for editor-driven typing. mypy is widely deployed in CI. Many projects run both.

mypy strictness flags

By default mypy is loose. Tighten gradually:

# pyproject.toml
[tool.mypy]
python_version = "3.11"
strict = true                       # turns on most strict flags

# Or individually:
disallow_untyped_defs = true        # require type annotations on every function
disallow_any_generics = true        # forbid `list` instead of `list[int]`
disallow_untyped_calls = true       # don't allow calling untyped funcs from typed code
warn_return_any = true              # error if a function returns Any
warn_unused_ignores = true          # flag stale `# type: ignore`
no_implicit_optional = true         # `def f(x: int = None)` requires Optional[int]
check_untyped_defs = true           # type-check bodies of untyped functions too

pyright equivalents

{
  "typeCheckingMode": "strict",
  "reportMissingTypeStubs": "warning",
  "reportUnknownArgumentType": "warning",
  "reportPrivateUsage": "warning"
}

Or in pyproject.toml:

[tool.pyright]
typeCheckingMode = "strict"

Modes: off, basic, standard, strict. Pick basic to start, escalate per directory.

Common errors and how to handle them

Any returned from untyped libraries

import some_untyped_lib
data = some_untyped_lib.fetch()   # Any

Options: install stubs (types-some-lib from typeshed), write your own .pyi, or accept and cast:

from typing import cast
data = cast(dict[str, str], some_untyped_lib.fetch())

# type: ignore for unreachable cases

import platform
if platform.system() == "Windows":
    import winreg   # type: ignore[import-not-found]

Add the error code so future edits don’t silently mask new issues.

Narrowing with assert

When checker can’t prove a type:

def f(x: int | None) -> int:
    assert x is not None
    return x + 1

assert removed by python -O, so use only in dev-time checks.

Runtime enforcement (when you actually need it)

Type hints are not enforced at runtime. If you need that:

Tool Use case
pydantic Models, API request/response, config — validates and coerces
attrs + cattrs Plain attrs with optional validation/conversion
typeguard Decorator that wraps a function with runtime type-checking
beartype Like typeguard but micro-fast, near-zero overhead
dataclasses No validation by default — pair with pydantic if needed

Example with beartype:

from beartype import beartype

@beartype
def add(a: int, b: int) -> int:
    return a + b

add(1, "x")   # raises BeartypeCallHintParamViolation at call time

Interview angle

“What’s the difference between mypy and pyright?” (Two implementations of static type checking — pyright is faster and stricter; mypy has more plugins.) “Are type hints enforced at runtime?” (No, unless you use pydantic / beartype / typeguard.) Senior follow-up: “How would you add type checking to a 100k-LOC codebase that has none?” → start permissive, add disallow_untyped_defs per package, use # type: ignore sparingly with codes.