pyright
The pyright package on PyPI is not Microsoft's type checker itself. Microsoft's pyright is a TypeScript program distributed on npm, and this package is Robert Craigie's community wrapper around it; the README opens by stating it is not affiliated with Microsoft in any way. What pip installs is a launcher: when you run the pyright command it looks for node on your PATH, downloads a Node.js runtime via nodeenv if there is none (or via prebuilt wheels if you installed the nodejs extra), npm-installs the pinned pyright package into a cache directory under ~/.cache, and then forwards your arguments to the real checker. From there the flags, config files, and output are exactly upstream pyright, the same engine that powers Pylance in VS Code. Each wrapper release pins one upstream version, and a set of PYRIGHT_PYTHON_* environment variables controls which version actually runs, where node comes from, and where the cache lives.
The path of least resistance for getting pyright into a pip-only workflow, provided you accept a Node.js runtime appearing in ~/.cache and a community wrapper that trails upstream on its own schedule. If node is already in your stack, install the npm package directly and skip the layer.
Use it if
- Your team is pip-only and you want pyright in requirements.txt, tox, or CI without asking every Python developer to install Node.js and npm first
- You want CI to run the same checker as Pylance in VS Code, so the squiggles developers see locally match what the pipeline rejects
- You use pre-commit: this repo is the maintained pre-commit hook for pyright, with a documented fix for the virtualenv isolation problem
- You need version control over the checker: the pip version pins one upstream release, and PYRIGHT_PYTHON_FORCE_VERSION lets you pin or float independently of your lockfile
- Your build or CI environment blocks outbound network at runtime: on first run this package downloads a Node.js runtime and an npm package, which fails hard in air-gapped or allowlist-only environments unless you pre-warm the cache
- Node is already part of your stack: npm install pyright gives you the same checker with one fewer layer, no runtime download step, and no wrapper between you and upstream
- You cannot tolerate lag behind upstream: wrapper releases are automated but gapped in practice (v1.1.408 on 2026-01-08, then nothing until v1.1.409 on 2026-04-23) while Microsoft ships pyright roughly weekly, so you end up managing PYRIGHT_PYTHON_FORCE_VERSION yourself
- You are uncomfortable with a single-maintainer, 273-star community project sitting in the critical path of your type checking, downloading and executing code at runtime
- You want a checker that is itself Python: mypy needs no node at all, and basedpyright publishes real wheels with the checker bundled instead of fetched on first run
Setup reality
pip install pyright succeeds instantly because you only installed the launcher; the real work happens on first invocation. If node is not on PATH, nodeenv downloads a full Node.js distribution, then npm installs the actual checker into ~/.cache (override with PYRIGHT_PYTHON_CACHE_DIR or XDG_CACHE_HOME). The README itself recommends pip install pyright[nodejs] instead, which pulls node from nodejs-wheel-binaries wheels because the nodeenv path is less reliable. In CI this means the first pyright call costs a download every run unless you cache that directory. Version behavior surprises people: the pip version pins one npm version, PYRIGHT_PYTHON_FORCE_VERSION overrides it, and setting it to latest means results can change with no change to your lockfile. Under pre-commit, the hook runs in its own virtualenv and cannot see your dependencies until you add additional_dependencies or point venvPath at your real venv.
Patterns
Install and run the checkerinstall-and-run
pip install "pyright[nodejs]"
pyright src/ # check a directory
python -m pyright src/ # same thing, module form
pyright --version # triggers the first-run downloadThe nodejs extra pulls Node.js from prebuilt wheels (nodejs-wheel-binaries), which the README recommends over the default nodeenv download because it is more reliable. Either way, the first invocation still npm-installs the actual checker into ~/.cache.
Pin or float the upstream pyright versionpin-pyright-version
# run an exact upstream version, regardless of what pip installed
export PYRIGHT_PYTHON_FORCE_VERSION=1.1.411
# or always track Microsoft's newest release
export PYRIGHT_PYTHON_FORCE_VERSION=latest
pyright src/Each pip release pins one npm version, and the wrapper's releases can trail upstream by weeks. latest closes that gap but means new diagnostics appear in CI with no change on your side; pin an exact version for reproducible builds. PYRIGHT_PYTHON_PYLANCE_VERSION instead matches your VS Code Pylance build, but FORCE_VERSION wins if both are set.
Configure with pyrightconfig.jsonpyrightconfig-basics
{
"include": ["src"],
"exclude": ["**/__pycache__", ".venv"],
"typeCheckingMode": "standard",
"pythonVersion": "3.11",
"venvPath": ".",
"venv": ".venv"
}venvPath plus venv is how pyright finds your installed packages when it is not launched from inside the venv; without them you get reportMissingImports on every third-party import. pythonVersion controls which syntax and stdlib stubs are assumed, independent of the interpreter running the wrapper.
Configure in pyproject.toml insteadpyproject-tool-pyright
[tool.pyright]
include = ["src"]
typeCheckingMode = "strict"
pythonVersion = "3.11"
venvPath = "."
venv = ".venv"Same keys as pyrightconfig.json, TOML syntax. If both files exist, pyrightconfig.json wins and the pyproject section is ignored entirely, which is a classic source of edits that mysteriously do nothing.
Run pyright in GitHub Actionsrun-in-ci
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt "pyright[nodejs]"
- run: pyrightInstall your project's dependencies into the same environment or every import gets flagged. The exit code is nonzero when errors exist, so the step fails the build on its own. The first pyright call downloads the npm package every run; cache ~/.cache (or set PYRIGHT_PYTHON_CACHE_DIR to a cached path) to avoid paying that each time.
Get JSON output for toolingmachine-readable-output
pyright --outputjson src/ > report.json
python -c "
import json
r = json.load(open('report.json'))
print(r['summary']['errorCount'], 'errors')
for d in r['generalDiagnostics'][:5]:
print(d['file'], d['range']['start']['line'] + 1, d.get('rule'), d['message'].splitlines()[0])
"generalDiagnostics carries file, severity, rule, and a range; line numbers in ranges are 0-based, so add 1 before showing them to humans. summary has errorCount, warningCount, and timeInSec, handy for tracking a ratchet number in CI.
Re-check on every savewatch-mode
pyright --watch src/Keeps the node process alive and re-analyzes only what changed, so iterations are much faster than fresh runs, which rebuild the whole program state. The tradeoff is a resident node process holding the analysis in memory.
Suppress a diagnostic on one lineignore-per-line
value = legacy_call() # pyright: ignore[reportUnknownVariableType]
data = parse(raw) # type: ignorePrefer the pyright: ignore form with a rule name: it suppresses only that rule, only for pyright, so mypy still checks the line. A bare type: ignore silences every rule for every checker and quietly keeps hiding new problems introduced later.
Relax rules or mode for a whole fileignore-per-file
# at the top of the file
# pyright: reportMissingTypeStubs=false, reportUnknownMemberType=false
# or override the checking mode for just this file
# pyright: basicThese comments must appear before any code. Useful for generated code or a legacy module inside a strict repo; the rest of the codebase keeps its configured mode. Rule names are the report* settings from the configuration docs.
Ratchet from basic to strict, directory by directorystrict-vs-basic-mode
{
"typeCheckingMode": "basic",
"strict": ["src/core"]
}strict enables dozens of extra report* rules (unknown types, missing annotations, untyped decorators), and turning it on repo-wide in an existing codebase usually produces thousands of errors. The strict array lets you hold cleaned-up directories at strict while the rest stays basic. Note the default mode is standard, which sits between the two.
Run pyright and mypy side by sideuse-with-mypy
pip install mypy "pyright[nodejs]"
mypy src/ && pyright src/They genuinely disagree: pyright infers types inside unannotated functions while mypy skips unannotated bodies by default, and their narrowing rules differ. Expect each to report things the other accepts. Use type: ignore for both checkers and pyright: ignore[rule] when only pyright should stand down.
Run as a pre-commit hookpre-commit-hook
repos:
- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.411
hooks:
- id: pyright
additional_dependencies: ["pytest", "pydantic"]pre-commit installs the hook in its own isolated virtualenv, so pyright cannot see your project's packages and floods you with import errors. The README's two fixes: list your dependencies under additional_dependencies, or set venvPath and venv in your pyright config to point at the real project venv.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| mypy | PyPI | You want the reference Python type checker as a plain Python package, with zero Node.js involvement and the plugin ecosystem (Django, SQLAlchemy stubs) |
| basedpyright | PyPI | You want pyright's engine but properly packaged for PyPI: the checker ships inside the wheel, nothing is downloaded at runtime, plus extra rules and a real LSP story outside VS Code |
| ty | PyPI | You want a very fast Rust-based checker from the ruff team and can accept something much newer than pyright or mypy |