Skip to content
v0.1.0latest release21 skills2 categories3 sub-categories22 tested scripts68 reference docsContact us

Type Checker Setup

Reference for python-type-hints-annotator · 312 lines

Programming / Pythonv1.0.0Updated OCT 20 20265 references1 script

Configuring mypy and pyright, running them in CI, and fixing the errors they report. Short notes on pyrefly and ty at the end.

Contents

Which checker

mypypyright (and Pylance in VS Code)
ImplementationPython, plugin systemTypeScript/Node, very fast, incremental
Default on unannotated functionsskips the body (unless check_untyped_defs)checks everything, infers return types
Pluginsyes (pydantic, SQLAlchemy 1.x, django-stubs)no plugins; relies on dataclass_transform and stubs
Inferenceconservative, explicitaggressive narrowing and literal inference
Editorvia LSP pluginsbuilt into Pylance / basedpyright
Best forCI gate for typed libraries, plugin-dependent frameworkseditor feedback, large codebases, strictest checking

Many teams run pyright (or Pylance) in the editor and mypy (or pyright) in CI. Pick one checker as the CI source of truth; running both doubles the suppression work because they disagree on edge cases.

mypy configuration

pyproject.toml:

toml
[tool.mypy]
python_version = "3.11"
files = ["src", "tests"]
strict = true                       # see the flag list below
warn_unreachable = true
enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool", "possibly-undefined"]
plugins = ["pydantic.mypy"]
mypy_path = "stubs"
exclude = ["^build/", "^notebooks/"]

[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true
warn_required_dynamic_aliases = true

# untyped third-party libraries
[[tool.mypy.overrides]]
module = ["faiss.*", "sentence_transformers.*", "legacy_rerank.*"]
ignore_missing_imports = true

# legacy code being migrated: checked, but not strict yet
[[tool.mypy.overrides]]
module = ["app.legacy.*"]
disallow_untyped_defs = false
disallow_incomplete_defs = false
check_untyped_defs = true

# tests: allow unannotated test functions
[[tool.mypy.overrides]]
module = ["tests.*"]
disallow_untyped_defs = false

Run: mypy (uses files), mypy src/app/llm.py, mypy -p app. Useful CLI flags: --strict, --python-version 3.10, --show-error-codes (default on), --pretty, --install-types --non-interactive (fetch missing types-* stubs), --cache-dir, --html-report out/ / --txt-report (needs lxml for HTML) for coverage of typed lines.

pyright configuration

pyproject.toml (or pyrightconfig.json with the same keys):

toml
[tool.pyright]
include = ["src", "tests"]
exclude = ["**/__pycache__", "build", "notebooks"]
pythonVersion = "3.11"
typeCheckingMode = "strict"          # "off" | "basic" | "standard" (default) | "strict"
stubPath = "stubs"
venvPath = "."
venv = ".venv"
reportMissingTypeStubs = false       # noisy for libraries without stubs
reportUnknownMemberType = false      # relax individual strict rules while migrating
reportUnnecessaryTypeIgnoreComment = "warning"
reportImplicitOverride = "warning"   # require @override (opt-in)
strict = ["src/app/core"]            # strict for these paths even if the default mode is lower

Per-file: # pyright: strict or # pyright: basic at the top of a file; per-rule: # pyright: reportUnknownMemberType=false.

Run: pyright / npx pyright / pip install pyright (wraps the Node package), pyright --verifytypes mypkg (reports the completeness of a library's public API types; great before adding py.typed), pyright --outputjson. basedpyright is a community fork with extra rules and a baseline file feature.

Strictness flags explained

mypy strict = true enables:

FlagEffect
disallow_untyped_defsevery function needs full annotations
disallow_incomplete_defspartially annotated functions are errors
check_untyped_defstype-check bodies of unannotated functions
disallow_untyped_callsno calls from typed into untyped functions
disallow_untyped_decoratorsdecorators must be typed (else they erase the signature)
disallow_any_genericslist → must be list[str]
disallow_subclassing_anyno subclassing classes from untyped modules
warn_return_anyreturning Any from a typed function
warn_unused_ignoresstale # type: ignore
warn_redundant_castscast(str, x) where x is already str
no_implicit_reexportfrom mod import X in mod2 does not re-export X unless __all__ or import X as X
strict_equality== between unrelated types
extra_checksstricter TypedDict / **kwargs / overload rules

Not in strict, worth enabling: warn_unreachable, disallow_any_unimported, disallow_any_explicit (very strict), enable_error_code = ["ignore-without-code", "explicit-override", "possibly-undefined", "truthy-bool"].

pyright modes:

ModeAdds
basiccore errors: unknown attributes, wrong arguments, missing imports
standard (default)+ override compatibility, protocol/TypedDict checks, possibly-unbound variables
strict+ reportUnknown* (any value inferred as Unknown), missing parameter types, unused imports/variables, private usage, unnecessary isinstance/casts

Running in CI and pre-commit

GitHub Actions:

yaml
name: types
on: [push, pull_request]
jobs:
  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: pip
      - run: pip install -e ".[dev]"             # includes mypy/pyright, stubs, plugins
      - run: mypy
      - run: python tools/annotation_audit.py src --min-coverage 85   # vendored copy of this skill's script

pre-commit (mypy runs in an isolated env, so list the stubs and typed deps it needs):

yaml
repos:
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.13.0
    hooks:
      - id: mypy
        additional_dependencies: [pydantic, types-requests, types-PyYAML, numpy]
        args: [--config-file=pyproject.toml]
  - repo: https://github.com/RobertCraigie/pyright-python
    rev: v1.1.389
    hooks:
      - id: pyright

Pin the checker version (mypy==1.x.y, pyright==1.1.xyz); new releases add checks and will break CI unannounced otherwise. Use ruff's UP (pyupgrade) rules (UP006, UP007, UP035, UP040, UP045, UP046, UP047) to autofix legacy typing syntax, and ANN rules to require annotations, and set ruff's target-version to the same minimum Python.

Suppressing errors correctly

python
result = legacy.score(q)  # type: ignore[no-untyped-call]  # vendored lib, no stubs (TICKET-123)
value = data["k"]         # pyright: ignore[reportTypedDictNotRequiredAccess]
DoDon't
# type: ignore[error-code] with a reasonbare # type: ignore
Fix the root type once (adapter, stub, TypedDict)sprinkle ignores at every call site
cast(T, x) only after a check the checker cannot seecast to silence a real mismatch
typing.TYPE_CHECKING for annotation-only importsAny to avoid an import cycle
enable warn_unused_ignores / reportUnnecessaryTypeIgnoreCommentlet stale ignores pile up

assert isinstance(x, T) also narrows, and documents an invariant (but is stripped with python -O; raise explicitly for real validation).

Debugging types

python
from typing import reveal_type, assert_type        # 3.11 (typing_extensions before)

reveal_type(store.search(q))                        # checker prints the inferred type
assert_type(first(["a"]), str)                      # checker error if the type differs

reveal_type without an import also works in mypy and pyright (they treat it as a builtin), but at runtime it is a NameError before 3.11 unless imported. reveal_locals() (mypy only) prints all local types. In VS Code, hover shows pyright's inferred type.

Error catalogue with fixes

MessageFix
mypy: Item "None" of "Client | None" has no attribute "complete" [union-attr]; pyright: "complete" is not a known attribute of "None" (reportOptionalMemberAccess)narrow: if self.client is None: raise RuntimeError("not connected"); copy attributes to a local before narrowing
mypy: Incompatible default for argument "stop" (default has type "None", argument has type "list[str]") [assignment]; pyright: Expression of type "None" cannot be assigned to parameter of type "list[str]"stop: list[str] | None = None
mypy: Argument 1 to "f" has incompatible type "str | None"; expected "str" [arg-type]; pyright: reportArgumentTypehandle None before the call, or os.environ["KEY"] instead of .get for required settings
mypy: Incompatible return value type (got "str | None", expected "str") [return-value]; pyright: reportReturnTypereturn a default, raise, or widen the return type
pyright: Object of type "None" is not subscriptable (reportOptionalSubscript)same narrowing

Annotations missing or incomplete

MessageFix
Function is missing a type annotation [no-untyped-def]annotate all params and the return
Function is missing a return type annotation [no-untyped-def] (often __init__)-> None
Call to untyped function "load" in typed context [no-untyped-call]annotate load, or stub the library
Need type annotation for "cache" (hint: "cache: dict[<type>, <type>] = ...") [var-annotated]cache: dict[str, list[float]] = {}
Missing type parameters for generic type "dict" [type-arg]dict[str, object]
pyright strict: Type of parameter "x" is unknown (reportUnknownParameterType), Type of "y" is partially unknown (reportUnknownVariableType)annotate; the Unknown usually comes from an untyped library: wrap it
Untyped decorator makes function "handler" untyped [misc]type the decorator with ParamSpec

Container variance and generics

MessageFix
Argument 1 to "mean" has incompatible type "list[int]"; expected "list[float]" + note "List" is invariantaccept Sequence[float]
Argument 1 has incompatible type "dict[str, int]"; expected "dict[str, float | int]"accept Mapping[str, float]
Value of type variable "T" of "f" cannot be "X" [type-var]the argument violates the bound; widen the bound or fix the call
A function returning TypeVar should receive at least one argument containing the same TypeVar [type-var]the type parameter appears only in the return: use a concrete type or object
pyright: TypeVar "T" appears only once in generic function signaturereplace it with the bound (or object)

Classes and inheritance

MessageFix
Signature of "retrieve" incompatible with supertype "Retriever" [override] / reportIncompatibleMethodOverrideparameters may only be widened and returns only narrowed; keep parameter names (callers may use keywords)
Cannot instantiate abstract class "Base" with abstract attribute "run" [abstract]implement the method in the subclass
"Settings" has no attribute "api_key" [attr-defined]declare it in the class body: api_key: str
Cannot assign to final name "MAX_TOKENS" [misc]it is Final; use a new name or drop Final
Incompatible types in assignment (expression has type "int", base class "Base" defined the type as "str")same type in subclass, or make the base generic
Only concrete class can be given where "type[Base]" is expected [type-abstract]pass a concrete subclass or accept Callable[[], Base]

Calls

MessageFix
Unexpected keyword argument "temprature" for "complete" [call-arg] / reportCallIssuereal typo: fix the call
Missing positional argument "model" in call to "Client" [call-arg]pass it, or give it a default
Too many arguments for "f" [call-arg]check the signature (and decorators that may have erased it)
No overload variant of "complete" matches argument types "str", "bool" [call-overload]add a catch-all bool overload
"Coroutine[Any, Any, str]" has no attribute "upper" / reportAttributeAccessIssuemissing await
Value of type "Coroutine[Any, Any, None]" must be used [unused-coroutine]missing await (or asyncio.create_task(...))

Imports and modules

MessageFix
Library stubs not installed for "requests" [import-untyped]pip install types-requests
Skipping analyzing "faiss": module is installed, but missing library stubs or py.typed marker [import-untyped]override ignore_missing_imports = true for that module, wrap it in an adapter
Cannot find implementation or library stub for module named "app.x" [import-not-found] / reportMissingImportsrun the checker from the project root / set mypy_path, packages, or pyright extraPaths; ensure the venv is the one being checked
Module "app.models" does not explicitly export attribute "Message" [attr-defined]import from the defining module, or re-export with __all__ / from .x import Message as Message
Name "torch" is not defined at runtime after moving imports under TYPE_CHECKINGadd from __future__ import annotations or quote the annotation

TypedDict, Literal, Protocol

MessageFix
TypedDict "Message" has no key "tool_calls" [typeddict-unknown-key]add the key (as NotRequired) or use a different TypedDict
Missing key "content" for TypedDict "Message" [typeddict-item]supply it, or mark it NotRequired
Argument 1 has incompatible type "str"; expected "Literal['user', 'assistant']"the variable was widened to str: annotate it with the Literal type or use Final
Argument 1 has incompatible type "OpenAIEmbedder"; expected "Embedder" + Following member(s) ... have conflictsthe method signature differs from the protocol (often a parameter name or list vs Sequence); align it
Only @runtime_checkable protocols can be used with instance and class checksadd @runtime_checkable or check differently

Any leaks

MessageFix
Returning Any from function declared to return "dict[str, str]" [no-any-return]validate (TypeAdapter(...).validate_python(data)), or annotate an intermediate variable after an isinstance check
Expression has type "Any" [misc] (with disallow_any_expr)type the boundary that produced it
Class cannot subclass "Module" (has type "Any") [misc]install the library's types or stub the base class

pyrefly and ty

Two newer checkers written in Rust focus on speed (whole-repo checks in seconds) and IDE integration:

pyrefly (Meta)ty (Astral, makers of ruff and uv)
Runpip install pyrefly, pyrefly init, pyrefly checkuvx ty check
Configpyrefly.toml or [tool.pyrefly]ty.toml or [tool.ty]
Migrationpyrefly init can translate an existing mypy/pyright config
Status (2025–2026)young, evolving quicklyyoung, evolving quickly

Both aim at compatibility with the typing spec, but their error codes and inference differ from mypy and pyright. Treat them as fast extra feedback (or evaluate them for the editor) and keep a mature checker as the CI gate until your codebase is clean under the new tool. Annotations written per the typing spec work unchanged in all four checkers.

More in Python

python-beginner-concept-tutor

Teach Python concepts to beginners heading into AI engineering (data scientists, analysts, students, career switchers): plain-language definitions, analogies, minimal examples, line-by-line walkthroughs, common mistakes, check-your-understanding questions and mini exercises; Socratic hints before solutions; explaining tracebacks; mutable vs immutable, references and aliasing, is vs ==, integer division, floating point, slicing and off-by-one, indentation errors; a staged learning path from variables to calling an LLM API, embeddings and a tiny RAG. Ships a script that lists the concepts a snippet uses in learning order and traces execution step by step like Python Tutor.

python-comprehension-refactorer

Refactor Python loops into list, dict and set comprehensions and generator expressions, and unroll comprehensions that are too clever: filtering, mapping, nested-loop flattening, conditional expressions, walrus in comprehensions, dict inversion, grouping with defaultdict/Counter, lazy generators for sum/any/all/max, streaming large files, itertools (chain, islice, groupby, batched, pairwise, accumulate, product), scoping rules, side-effect anti-patterns and performance (when map/filter or numpy/pandas vectorization win). AI-engineering focus: building prompt lists, batching texts for embeddings, filtering retrieved chunks by score, id->embedding dicts, flattening tool-call results, lazy dataset streaming and token counting.

python-context-manager-writer

Write correct Python context managers and find resource-handling bugs — __enter__/__exit__ semantics and exception suppression, contextlib (contextmanager, asynccontextmanager, ExitStack, AsyncExitStack, closing, aclosing, suppress, redirect_stdout, nullcontext, chdir), async with, re-entrancy, leaked files/connections/clients/pools/locks, and AI-engineering recipes: torch.no_grad/inference_mode/autocast, eval()/train() switches, GPU cache cleanup, token and latency meters, tracing spans, temp env vars, rate-limit semaphores, httpx/aiohttp client lifecycle, FastAPI lifespan, DB/vector-store transactions, atomic checkpoint writes, seeds and agent sandboxes.

python-dataclass-converter

Convert boilerplate Python classes, dicts, tuples and JSON payloads into dataclasses, or into pydantic v2 BaseModel, TypedDict, NamedTuple, attrs or msgspec when that fits better. Covers every @dataclass option (frozen, slots, kw_only, order, unsafe_hash, match_args, weakref_slot), field() (default_factory, init=False, repr, compare, hash, metadata), __post_init__, InitVar, KW_ONLY, inheritance and default ordering, replace(), asdict deep-copy pitfalls, hashing/equality, serialization and validation. AI focus: LLM request/response and chat Message types, RAG chunks with embeddings (numpy eq gotcha), eval records, configs, typed OpenAI/Anthropic JSON.

python-exception-handling-designer

Design and review Python exception handling: custom exception hierarchies with retryable flags, what to catch where, EAFP vs LBYL, chaining with raise ... from, else/finally, logger.exception, add_note, ExceptionGroup and except* with asyncio.TaskGroup, warnings, asserts vs validation, FastAPI error translation, retries with backoff and jitter, timeouts, cancellation, pytest.raises, plus LLM-app failures: 429 rate limits with retry-after, 5xx overloaded, context-length exceeded, content filters, broken streams, malformed JSON repair loops, tool-call errors fed back to the model, partial embedding batch failures, CUDA OOM batch halving, vector DB outages, circuit breakers and model fallbacks.

python-idiomatic-code-reviewer

Review Python code for non-idiomatic, unpythonic or risky patterns and rewrite it into clean modern Python: PEP 8 / PEP 20, EAFP vs LBYL, iteration idioms (enumerate, zip, comprehensions, any/all), unpacking, f-strings, pathlib, truthiness, `is None`, mutable defaults, walrus, match statements, collections / itertools / functools, logging instead of print, with-blocks and resource handling. Includes AI-engineering checks: hardcoded API keys, time.sleep or requests inside async def, missing HTTP timeouts, unbounded retry loops, model clients built at import time, prompt string concatenation, sequential awaits, eval() on LLM output. Ships a 46-rule AST reviewer with severities and suggested rewrites.