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.