Strict pyright on a giant monorepo is correct—and useless if every PR waits an hour. The unfair advantage is affected-package analysis: typecheck only dirty projects plus dependents, keep a warm cache, and reserve full-fleet runs for main/nightly.
⚡ TL;DR: Partition with
pyrightconfig.json/ polyrepo packages; compute changed paths → package set → dependents; runpyright -pper package with CI cache onpyrightartifacts; full check onmain. Pair with TypeScript Project References and AST-Guided Edits.
The failure mode of “just run pyright .”
A single root invocation:
- Re-walks the entire import graph on every PR
- Contends for CPU with tests and lint in the same job
- Encourages teams to flip
typeCheckingModetobasic“temporarily” - Hides which package introduced the break when the log is 40k lines
Incremental CI keeps strict mode and shrinks the workset, not the rules.
Package graph, not file list
# ✅ Example: git-diff → packages (illustrative)
git diff --name-only origin/main...HEAD \
| awk -F/ '/^packages\//{print $2}' \
| sort -u > /tmp/changed_pkgs.txt
# ✅ Expand dependents from a simple adjacency file
# packages/deps.json: {"payments": ["ledger", "api"], "api": []}
import json
from pathlib import Path
deps = json.loads(Path("packages/deps.json").read_text())
# reverse edges: who imports me?
rev: dict[str, set[str]] = {k: set() for k in deps}
for pkg, uses in deps.items():
for u in uses:
rev.setdefault(u, set()).add(pkg)
def closure(changed: set[str]) -> set[str]:
out = set(changed)
stack = list(changed)
while stack:
p = stack.pop()
for d in rev.get(p, ()):
if d not in out:
out.add(d)
stack.append(d)
return out
# ✅ CI job sketch
# - restore cache key: pyright-${{ hashFiles('**/pyrightconfig.json', 'uv.lock') }}
# - for pkg in $(cat affected); do pyright -p packages/$pkg; done
# - on main: pyright -p . # full
Generate deps.json from your packaging tool (Pants, Bazel, uv workspaces, poetry plugins) so humans do not hand-edit edges forever. Treat drift as a CI failure.
Config that keeps strictness
{
"include": ["src"],
"exclude": ["**/tests/fixtures", "**/generated"],
"typeCheckingMode": "strict",
"venvPath": ".",
"venv": ".venv",
"reportMissingImports": true,
"pythonVersion": "3.12"
}
Per-package configs should extend a shared base rather than fork rules:
{
"extends": "../../pyright.base.json",
"include": ["src"],
"executionEnvironments": [{ "root": "src" }]
}
| Mode | When |
|---|---|
| Affected + dependents | PR / push branches |
| Full workspace | main, nightly, release |
| Changed files only | ❌ misses cross-package breakages |
❌ pyright $(git diff --name-only) on raw files without dependents — you green PRs that break importers.
Caching that actually hits
- Cache key:
pyright-${{ runner.os }}-${{ hashFiles('**/pyrightconfig*.json', 'uv.lock', '**/py.typed') }} - Store the pyright binary / nodeenv separately from package type artifacts
- Prefer
uv sync --frozenbefore pyright so stub packages are identical across runners - Shard by package on large PRs (
matrix.package) but merge results into one check run
Agent-friendly build graph
Coding agents thrash CI when they cannot see package boundaries. Check in a machine-readable graph (same idea as TypeScript Project References) and teach agents to edit within one package + run that package’s pyright locally before opening a PR. Pair with AST-Guided Edits so refactors do not spray Any across the monorepo.
Escape hatches that do not rot
Allow # pyright: ignore only with a tracking issue ID in the same line. Ban repo-wide typeCheckingMode: off. If a generated client is untyped, put it under exclude and wrap it with a typed facade package.
Nightly full gate as the contract
PR incremental checks are a filter, not the source of truth. Nightly (and every merge to main) must still run the full strict workspace. If nightly fails while PRs were green, you have a graph bug—usually a missing reverse edge—or an unchecked generated path. Page the owning platform team; do not silently downgrade strictness overnight.
Measuring the win
Track PR wall-clock for the typecheck job before/after incremental rollout. Healthy targets: median PR typecheck under 5–8 minutes on a 1M-LOC monorepo, with nightly full under whatever budget finance already accepted. Publish a weekly dashboard of “PRs that would have missed a dependent break under file-only mode” by replaying diffs — that proves why dependents stay mandatory.
Closing checklist
✅ Dos
– ✅ Maintain an explicit package dependency graph
– ✅ Typecheck dependents of changed packages
– ✅ Cache pyright / nodeenv between CI runs
– ✅ Keep main full-strict as the source of truth
– ✅ Fail CI on config drift (pyrightconfig owned)
– ✅ Shard by package on large affected sets
– ✅ Require issue IDs on inline ignores
❌ Don’ts
– ❌ Don’t lower typeCheckingMode to save wall time
– ❌ Don’t skip dependents to “go faster”
– ❌ Don’t commit generated files into the typecheck set without need
– ❌ Don’t run one giant process with no cache on every PR
– ❌ Don’t let agents open PRs without a local package-scoped pyright run
Related reading
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
