Filter concepts by levelShowing all levels.

Python · Type Hints and Static Typing

Tools

Concepts
2

The two mainstream static type checkers, and how a team actually puts one to use — in the editor, in strict mode, and as a mandatory CI check.

This section

Checking and enforcing types

The two real tools, and where running them actually stops a mistake from shipping.

mypy vs. pyright

coreintermediate

mypy and pyright are static type checkers — separate programs that read your annotations and flag mismatches before the code ever runs. Neither is part of Python itself; both must be installed and run explicitly.

Think of it as

A type checker is a proofreader, not a compiler — it reads the whole file, cross-checks every annotation against every use, and reports what does not add up, without ever executing a single line of your code. mypy is the original, Python-native proofreader; pyright is Microsoft's, built into the Pylance VS Code extension, generally faster and used by more editors out of the box.

bash
pip install mypy && mypy app.py
npx pyright app.py

What we're doing: Run a deliberately type-incorrect call through both mypy and pyright and compare their real output.

bad_typing.pypython
def add(a: int, b: int) -> int:
    return a + b

result = add("2", "3")
print(result)
4
"2" and "3" are str, not int — this violates the annotation, but Python itself runs the line without error (it concatenates the strings).
Output
mypy: error: Argument 1 to "add" has incompatible type "str"; expected "int"  [arg-type]
mypy: error: Argument 2 to "add" has incompatible type "str"; expected "int"  [arg-type]
pyright: error: Argument of type "Literal['2']" cannot be assigned to parameter "a" of type "int" in function "add" (reportArgumentType)
pyright: error: Argument of type "Literal['3']" cannot be assigned to parameter "b" of type "int" in function "add" (reportArgumentType)

Why this works: Both tools parse the same source file, build the same understanding of add's signature from its annotations, and independently conclude that "2" (a str) does not match the int parameter — mypy names the general type str, pyright is more precise and names the literal value "2". Neither tool ran add("2", "3"); both analyzed the code without executing it.

Assuming a type checker installed on one machine runs automatically for every contributor

Wrong

bash
# mypy only installed locally, never added to CI or pre-commit —
# a type error ships to main because nothing ran it automatically

Better

bash
# add it to CI (e.g. GitHub Actions) so every push is checked:
# - run: pip install mypy && mypy .
# and optionally as a pre-commit hook, so it runs before commit too

What you see: A type error that mypy would have caught instantly ships to production, because the check only ran on the one machine where a developer happened to install and remember to run it.

Why: A type checker installed locally is opt-in and easy to forget — it provides zero protection until it runs somewhere every change must pass through, which is exactly what wiring it into CI (see using-type-checking-in-practice) is for.

Two type checkers, same source of truth

mypy

  • +pip install mypy — Python-native, the original
  • +mypy file.py from the command line
  • +Widely used as the CI standard for typed Python

pyright

  • Microsoft; powers Pylance in VS Code
  • npx pyright file.py, or runs live in the editor
  • Generally faster; strong editor integration
  • mypy
    • pip install mypy — Python-native, the original
    • mypy file.py from the command line
    • Widely used as the CI standard for typed Python
  • pyright
    • Microsoft; powers Pylance in VS Code
    • npx pyright file.py, or runs live in the editor
    • Generally faster; strong editor integration

The same type error, reported by each tool

The same type error, reported by each tool
ToolCommandWhat it reports
mypymypy file.pyerror: Argument 1 to "add" has incompatible type "str"; expected "int" [arg-type]
pyrightpyright file.py (or npx pyright)error: Argument of type "Literal['2']" cannot be assigned to parameter "a" of type "int" (reportArgumentType)

Together

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

result = add("2", "3")  # both mypy and pyright flag this; Python itself runs it

Remember: mypy and pyright are separate installed tools, not part of Python — both read the same annotations and report mismatches without running the code.

See also: using type checking in practice · basic annotations · mypy and pyright

Using type checking in practice

standardintermediate

An editor can run a type checker live as you type (VS Code + Pylance uses pyright automatically). --strict turns on every optional check at once. Running the same check in CI is what actually stops a type error from merging.

Think of it as

IDE type checking is a spellchecker underlining a typo as you type — fast, local, easy to ignore. CI type checking is the same rule enforced at the mailbox before a letter can be sent — nothing merges until it passes, which is why a team's real guarantee comes from CI, not from an editor squiggle a developer can just not look at.

bash
mypy --strict app.py
# CI step (e.g. GitHub Actions):
# - run: pip install mypy && mypy .

What we're doing: Confirm --strict is a real mypy flag by running it against the same deliberately wrong call used to introduce mypy, and observe it still reports the errors under strict mode.

terminalbash
mypy --strict bad_typing.py
1
--strict enables every optional mypy check at once, including ones that flag missing annotations elsewhere in a file.
Output
bad_typing.py:4: error: Argument 1 to "add" has incompatible type "str"; expected "int"  [arg-type]
bad_typing.py:4: error: Argument 2 to "add" has incompatible type "str"; expected "int"  [arg-type]
Found 2 errors in 1 file (checked 1 source file)

Why this works: --strict still catches the same str-vs-int mismatch a plain mypy run does, plus it would also flag any function elsewhere in the file left completely unannotated — strict mode is a superset of the default checks, not a different set of rules.

Turning on --strict for the first time on a large, previously untyped codebase

Wrong

bash
# adding mypy --strict directly to CI on a codebase with years of
# untyped code produces hundreds of errors on day one — the team
# disables the CI check entirely out of frustration

Better

bash
# start unstrict, or scope strict mode to new/changed files only,
# then tighten incrementally:
# mypy --strict --exclude 'legacy/' .
# or use a per-module override in mypy.ini/pyproject.toml

What you see: CI is red on the first run with an overwhelming error count, and the team turns the check off rather than fixing it, losing the tool entirely.

Why: Strict mode assumes a fully or mostly annotated codebase — applying it all at once to legacy code produces more noise than signal. Ramping up gradually (looser rules first, or strict scoped to new code) keeps the check useful instead of getting disabled out of frustration.

Remember: An IDE runs a type checker live for one developer; CI runs it for everyone, on every change — CI is what actually prevents a type error from merging.

See also: mypy vs pyright

Advertisement