Enabling Pyright Strict Mode Incrementally
Don’t flip pyright to strict repo-wide. Use pyrightconfig.json with a strict: ["packages/core"] include list so only ready packages are strict, mark individual files with a # pyright: strict pragma, and scope per-directory rules with executionEnvironments. Ratchet packages from basic to strict one entry at a time, turning reportMissingTypeStubs from a warning into an error as stubs land.
Pyright has three type-checking modes — off, basic, and strict — and a global typeCheckingMode: "strict" will light up hundreds of reportUnknownMemberType and reportMissingTypeStubs diagnostics across an unprepared monorepo. Pyright (since the config gained per-path strictness) lets you apply strict to a curated set of paths while the rest stay at basic. This page walks the gradual rollout: include lists, per-file pragmas, and executionEnvironments, ending at a clean per-package ratchet.
Step 1: a basic-everywhere baseline
Start with basic as the floor so the whole repo is at least loosely checked, then declare which paths are strict. The strict array takes paths (not modules), evaluated relative to the config file.
// pyrightconfig.json — pyright 1.1.x
{
"typeCheckingMode": "basic",
"include": ["packages"],
"exclude": ["**/node_modules", "**/.venv", "**/build"],
"strict": ["packages/core", "packages/sdk"],
"reportMissingImports": "error"
}
What the analyzer sees: files under packages/core and packages/sdk are checked at full strict (every reportUnknown* becomes an error); everything else under packages stays at basic, where unknown types are tolerated. New packages default to basic until you add them to strict.
Step 2: promote individual files with a pragma
Before a whole package is ready, you can make one file strict with an inline pragma. This is the finest-grained ratchet step.
# packages/api/parser.py — pyright 1.1.x
# pyright: strict
def parse_response(payload: dict[str, int]) -> int:
return sum(payload.values())
The # pyright: strict comment overrides the file’s mode regardless of the directory’s setting. You can also flip individual rules per file — # pyright: reportUnknownMemberType=false — to land a strict file that still has one tolerated gap, then remove the suppression later.
Step 3: scope rules with executionEnvironments
executionEnvironments apply settings to a subtree — different pythonVersion, extra import roots, or relaxed rules for a legacy directory while the repo default stays strict.
// pyrightconfig.json — per-directory rule scoping, pyright 1.1.x
{
"typeCheckingMode": "strict",
"include": ["packages"],
"executionEnvironments": [
{
"root": "packages/legacy_etl",
"reportUnknownMemberType": "none",
"reportMissingTypeStubs": "none",
"extraPaths": ["packages/legacy_etl/vendor"]
},
{
"root": "packages/core"
}
]
}
Here the repo is globally strict, but packages/legacy_etl silences the two noisiest unknown-type diagnostics and adds a vendored import path. packages/core inherits the strict default with no relaxations.
# pyright: pragma overrides the matching executionEnvironments entry, which overrides the strict/include path settings, which override the top-level typeCheckingMode. So a single file can opt into strict ahead of its package, and a directory can opt out without touching siblings.
Step 4: ratchet basic → strict, then handle stubs
The migration loop per package: add it to strict, run pyright, fix or pragma-suppress the diagnostics, then remove suppressions over time. The most common blocker is third-party libraries without stubs, which strict mode reports as reportMissingTypeStubs.
// pyrightconfig.json — tighten stub policy as the package matures, pyright 1.1.x
{
"strict": ["packages/core"],
"reportMissingTypeStubs": "warning", // start as a warning…
// …then promote to "error" once stubs are installed or bundled
"stubPath": "typings" // local stub overrides for stubless deps
}
Drop a .pyi into typings/<package>/ for any dependency that ships none, then flip reportMissingTypeStubs to "error" so regressions can’t reintroduce an untyped dependency.
# pyright: strict pragma and the strict include list change only what pyright reports — they alter no runtime behaviour. A file that passes strict and one that's off execute identically; strict simply refuses to let unknown types through at analysis time.
Edge cases
- Pragma vs config conflict. A
# pyright: basicpragma wins even if the file’s directory is in thestrictlist. Audit pragmas before assuming a package is fully strict. reportMissingModuleSource. Installing a stub-only package (atypes-*distribution) with no runtime package present triggers this; it’s informational, distinct fromreportMissingImports, and usually safe to leave atwarning.extraPathsand namespace packages. Strict mode surfaces import resolution gaps thatbasictolerated. If a namespace package resolves at runtime but pyright reportsreportMissingImports, add its root toextraPathsin the relevantexecutionEnvironmentsentry.
Common mistakes
- Global
typeCheckingMode: "strict"on day one. Floods CI withreportUnknownMemberType,reportUnknownArgumentType, andreportMissingTypeStubsacross unprepared packages. Use thestrictpath list instead. - Leaving blanket per-file rule suppressions in place.
# pyright: reportUnknownMemberType=falseat the top of a strict file silently defeats the point. Treat each as a TODO and delete it once fixed. - Forgetting
reportMissingTypeStubsratchet. Leaving it atnonelets new stubless dependencies slip in unnoticed. Promote it toerroronce the package is clean.
FAQ
Does pyright share mypy’s [[tool.mypy.overrides]] mechanism?
No. Pyright uses pyrightconfig.json path-based strict/include, executionEnvironments, and per-file pragmas. The mypy equivalent is covered in per-package mypy overrides.
Can I keep pyright config in pyproject.toml instead?
Yes, under [tool.pyright], but the strict include list and executionEnvironments are most readable in a dedicated pyrightconfig.json. The two are mutually exclusive — pyright ignores [tool.pyright] if a pyrightconfig.json exists.