1.251 Type Checkers#
Type checkers compared: mypy, pyright, ty and basedpyright in Python; TypeScript and Flow in JavaScript — plus runtime enforcement.
At a glance#
What the research found
- TypeScript 7 is the Go rewrite and it has shipped. microsoft/TypeScript reports 88% Go through the GitHub languages API with TypeScript itself at 10%, and v7.0.2 is tagged from typescript-go. 274.2M weekly npm downloads. — The largest event in this category and it is two months old. The type system did not change – the implementation did, which is where behavioral differences hide. Pin the compiler version and treat a 6 to 7 move as a change to review. Verifying it is cheap: install both, run both, diff the diagnostics.
- JavaScript has no selection decision. TypeScript at 274.2M weekly against Flow’s 450,351 is roughly six hundred to one. — Every real question for a JavaScript reader is downstream of having chosen TypeScript: strictness, where in the pipeline it runs, and whether type-aware lint rules are worth their cost.
- ty has adoption running far ahead of its version – 8.6M weekly downloads and 19,580 GitHub stars, within a thousand of mypy’s, at version 0.0.75. — The Ruff pattern from 1.250 repeating, with higher stakes. A linter’s new rule is a warning; a checker’s changed inference is a failed build. Try it, do not gate CI on it. What decides its ceiling is not speed but whether a Rust checker can offer a plugin interface at all – Ruff’s architecture could not.
- A fork of Microsoft’s checker has 2.6M weekly downloads. basedpyright describes itself as a fork of pyright with ‘pylance features’; pyright is MIT but Pylance, the editor experience above it, is licensed to official VS Code builds. — A different openness question from 1.250’s, and conflating them misleads. There the constraint was copyleft and it bound a vendor DISTRIBUTING a product. Here every license is permissive and the constraint is a proprietary layer above an open core. 1.250 asked what you may do with the tool; this asks what the tool is allowed to do for you.
- Runtime enforcement out-downloads every static checker except mypy – beartype 14.9M and typeguard 13.4M weekly, against ty’s 8.6M and pyright’s 8.7M. — A survey covering only static checkers would miss most of how this problem is actually solved. Static and runtime catch disjoint sets: static finds the branch nobody ran, runtime finds the value nobody could have predicted. Both figures count installs rather than decisions, since these are often transitive dependencies.
Explainer
What a type checker is, and why your code needs one told about it#
A guide for someone who has seen mypy or tsc in a CI config and wants to know what it
does. Every term is defined before it is used.
The one-sentence version#
A type checker reads your code without running it and tells you where a value of one kind is being used as though it were another.
What a “type” is#
A type is the kind of a thing. 7 is a number. "seven" is a piece of text. [1, 2, 3]
is a list of numbers. A Customer object is a customer.
Some operations make sense for some types and not others. You can subtract numbers. You cannot subtract a customer from a piece of text — and if your program tries, it either crashes or, worse, quietly produces nonsense.
A type checker’s whole job is to find those mismatches before the program runs.
Why this is harder than it sounds#
In some languages every variable’s type is written down and enforced by the compiler. Python
and JavaScript are not those languages. Both were designed so you could write x = 7 and
then x = "seven" two lines later, and both ran for decades that way, accumulating an
enormous amount of code with no type information in it at all.
So type checking was added afterwards, and everything about how these tools behave follows from that one fact.
Annotations: telling the checker what you meant#
You add types as annotations — notes in the source that say what a thing is supposed to be:
def total(prices: list[int]) -> int:
return sum(prices)prices: list[int] says “this takes a list of whole numbers”, and -> int says “it gives
back a whole number”. Python itself ignores these completely. They exist for the checker,
and for humans reading the code. Nothing enforces them when the program runs.
TypeScript works the same way for JavaScript, with one extra step: annotations are not legal JavaScript, so a build step strips them out before the code runs.
Any: the escape hatch, and the most important idea here#
The tools could not demand annotations everywhere, because nobody was going to annotate a
million existing lines before getting any value. So both languages have a type that means
“stop checking here” — Any in Python, any in TypeScript.
Any is compatible with everything, in both directions. A value that is Any can be used as
anything, and anything can be used where Any is expected. That is what lets annotated and
unannotated code live in the same file, and it is called gradual typing.
It has a consequence people find surprising: the checker can be green and your program can
still be wrong. If a wrong value gets into an Any, it travels wherever it likes, and
nothing downstream will object.
That is not a bug anyone forgot to fix. It is the price of a type system that could actually be adopted, and every tool in this survey pays it.
What a type checker is not#
Not a linter#
A linter (surveyed in 1.250) also reads your code and complains about it. The difference is what it needs to know.
A linter reasons about the shape of the code: this variable is never used, this if can
never be true, this line is unreachable. All of that is visible in the structure.
A type checker needs to know what values are. “You are subtracting from total, and
total is a piece of text” requires tracking what total is through every assignment,
function call and return in the program. That is a much bigger question, and it is why type
checkers are slower and are usually a separate step.
There is a small overlap: some linter rules need the type checker’s help. A rule like “do not
await something that is not a Promise” cannot be answered from the shape of the code alone.
Those rules exist, and they are the expensive ones.
Not a formatter#
A formatter (1.253) rewrites your code’s layout. It never has an opinion about correctness. Different job entirely.
Not a test#
A test runs your code and checks what it does. A type checker never runs anything.
They catch different bugs, and neither replaces the other. A test proves that the path it exercises works. A type checker checks every path, including the ones no test covers — but only for the kind of mistake it can see.
Static and runtime: two answers to the same problem#
Everything above is static checking — “static” meaning the code is sitting still, not running.
There is another approach. Runtime type checkers read the same annotations but enforce them while the program runs, raising an error the moment a function actually receives the wrong thing.
Static: finds the bug without running anything. Sees every path, including untested ones. Can be wrong about code too dynamic to follow. Costs build time.
>Runtime: finds the bug only on paths that actually execute. Never wrong about what it saw, because it saw it. Costs production time.
Neither is better. They catch disjoint sets of bugs: static finds the branch nobody ran, runtime finds the value nobody could have predicted.
The clearest case for runtime checking is data arriving from outside — JSON from an API, a
config file, a database row, an LLM’s output. That data’s type is Any by construction,
because nothing could have known it in advance, so the static checker is blind there and
always will be. That is the one place static checking cannot be improved into working.
Why the tools disagree with each other#
Two checkers implementing the same rules will reject different programs. That surprises people, and the reason is worth understanding.
Both constantly meet values whose type they cannot determine. What they do then is a design choice:
- Stay quiet. Assume the unknown thing is fine and move on. Fewer complaints, and fewer caught bugs.
- Infer. Work out what the type must be from how the value is used. More caught bugs, and occasional confident wrongness.
- Demand. Refuse to proceed until a human writes an annotation. Thorough, and hostile to a codebase mid-adoption.
Python’s two main checkers pick differently on the most common version of this. Given a function with no annotations, mypy leaves the body alone — you have not opted in, so it stays out of your way. pyright reads it and infers. That single difference produces most of the disagreement between them, and it means the better tool depends on whether your code was typed from the beginning.
Plugins: why some frameworks need special help#
Some libraries produce types that are not visible in the source code.
Django’s ORM builds a model’s attributes when the class is created, by machinery that runs at import time. SQLAlchemy turns a class attribute into something whose real type differs from what is written. Pydantic generates a constructor from field declarations.
In every case the type a developer needs is real when the program runs and absent from the text the checker reads. A checker looking only at the source is either blind to those types or confidently wrong about them.
The fix is a plugin — code that teaches the checker what a particular framework actually produces. This matters more than it sounds: if you use these frameworks, whether your checker has a plugin for them is usually a bigger deal than how fast it is.
Where the time goes#
Three costs, roughly in order:
Reading your dependencies. The checker needs the types of everything you import, which means reading a large part of your installed packages. On many projects this dominates, and it is why the first run is so much worse than the second.
Working out the consequences. Types propagate: settling one can force the checker to revisit everything downstream.
Cold versus warm. Your editor keeps the analysis alive and updates it as you type. CI starts from nothing every run. These differ by roughly ten to one, so “our typechecking is slow” means different things depending on which one you measured.
The vocabulary, collected#
- Type — the kind of a value: number, text, list of customers.
- Annotation — a note in the source saying what type something is meant to be. Ignored at runtime in Python; stripped before running in TypeScript.
- Static analysis — examining code without running it.
- Gradual typing — a type system that tolerates unannotated code, so it can be adopted a piece at a time.
Any— the type meaning “stop checking here”. The reason gradual typing works and the reason a green checker does not mean a correct program.- Inference — the checker working out a type you did not write.
- Strictness — how much unchecked code the tool will tolerate. Most configuration is an argument about this.
- Stub (
.pyi,.d.ts) — a file containing only type information for code that has none, so a checker can understand a library it cannot read. py.typed— a marker file a Python package ships to say “my annotations are real, please use them”. Without it, consumers’ checkers ignore them.- Plugin — code that teaches a checker about a framework whose types are not in the source.
- Soundness — whether a checker passing actually guarantees no type errors. None of these are sound, on purpose.
If you remember three things#
- A green type checker does not mean a correct program.
Anyis an escape hatch that every one of these tools depends on to be adoptable, and a wrong value inside one travels silently. - The type of data arriving from outside is unknowable in advance. JSON, config, an LLM’s output — a static checker cannot help there, by construction, and that is the job of a runtime check or a validating model.
- Which checker is right for you depends on your codebase’s history, not on which is best. If it was typed from the start, the aggressive one finds more. If you are adding types to years of untyped code, the one that stays quiet where you have not spoken is the one you will still be using in six months.
S1: Rapid Discovery
S1 — Type Checkers#
A type checker verifies that the values flowing through your program are the kinds of thing the code expects. Unlike a linter, which reasons about the shape of the code, a type checker needs a type system — it has to know what every value is, tracked through every assignment, call and return. That is what it can do that a linter cannot, and it is why it costs what it costs.
Every figure below was read from a primary source on 2026-08-29: the PyPI JSON API, pypistats.org, the npm registry with api.npmjs.org, and the GitHub REST API.
The category, measured#
| Tool | Language | Version (date) | Weekly downloads | Stars | Open issues | License |
|---|---|---|---|---|---|---|
| TypeScript | JS/TS | 7.0.2 (2026-07-08) | 274,198,210 | 110,779 | 5,171 | Apache-2.0 |
@typescript/native-preview | JS/TS | 7.0.0-dev (2026-07-07) | 9,078,956 | — | — | Apache-2.0 |
| mypy | Python | 2.3.1 (2026-08-15) | 38,833,853 | 20,617 | 3,215 | MIT |
| beartype (runtime) | Python | 0.22.9 (2025-12-13) | 14,932,009 | — | — | MIT |
| typeguard (runtime) | Python | 4.6.0 (2026-07-26) | 13,395,203 | — | — | MIT |
| pyright | Python | 1.1.411 (2026-06-25) | 8,689,592 | 15,604 | 323 | MIT |
| ty | Python | 0.0.75 (2026-08-26) | 8,588,719 | 19,580 | 897 | MIT |
| basedpyright | Python | 1.39.10 (2026-08-13) | 2,588,100 | 3,562 | 650 | MIT |
| Flow | JS | 0.329.0 (2026-08-22) | 450,351 | 22,281 | 524 | MIT |
| pytype | Python | 2024.10.11 | 183,187 | 5,032 | 5 | Apache-2.0 |
| pyre-check | Python | 0.10.0 (2026-08-06) | 43,431 | 7,172 | 167 | MIT — archived |
What the numbers say#
TypeScript 7 is the Go rewrite, and it has shipped. microsoft/TypeScript now reports
88% Go through the GitHub languages API, TypeScript itself down to 10%, and v7.0.2 is
tagged from typescript-go. The compiler that essentially all typed JavaScript depends on
was reimplemented in another language and released as stable. It is the largest event in
this category and it is two months old.
JavaScript has no selection decision. TypeScript at 274.2M against Flow’s 450K is six hundred to one. Every real question for a JS reader is downstream of having chosen it.
ty has adoption running far ahead of its version. 8.6M weekly downloads and 19,580 stars — within a thousand of mypy’s — at version 0.0.75. That is the Ruff pattern from 1.250 repeating, and with higher stakes: a linter’s new rule is a warning, a checker’s changed inference is a failed build.
A fork of Microsoft’s checker has 2.6M weekly downloads. basedpyright describes itself as a fork of pyright with “pylance features”. pyright is open source; Pylance, the editor experience above it, is not. That is a different openness question from 1.250’s GPL finding — there the constraint was on what you may do with the tool, here it is on what the tool is allowed to do for you.
Runtime enforcement out-downloads every static checker except mypy. beartype at 14.9M and typeguard at 13.4M, against ty’s 8.6M and pyright’s 8.7M. A survey covering only static checkers would miss most of how this problem actually gets solved. (Both are often transitive dependencies, so the figures count installs rather than decisions.)
Three tools are in trouble and in three different ways — a distinction 1.250 had to make and this survey inherits. pyre-check is archived: the maintainer has said it is over. pytype has not released in 22 months and has 5 open issues: an empty tracker on a live project means it is being kept, but on a project that has not shipped in two years it more likely means nobody is filing. Flow is healthy and unchosen — shipped a week ago, pushed the day of reading, at six hundred to one. Those are different risks and a reader deserves them stated differently.
The shape of the category#
Unlike linting, this category does not split by extensibility — it splits by what the checker assumes about your codebase:
- Gradual — mypy, built for adding types to code that has none, module by module, with the plugin ecosystem that makes Django, SQLAlchemy and Pydantic checkable at all.
- Complete — pyright and ty, faster and more aggressive at inference, strongest on code that was typed from the start.
- Inferring — pytype, which told you what your untyped code already meant. Effectively gone, and the gap is open.
- Runtime — beartype and typeguard, which decline the static problem and check at the boundary instead.
The recurring decision is not which checker is fastest. It is whether your code was typed from the beginning — and, if it was not, whether the frameworks you use need a plugin that only mypy has.
S1 Approach — Type Checkers#
What this pass does#
A rapid survey of the category: what exists, who maintains it, how widely it is used, and what each one is actually for. No benchmarks — those are S2’s, and the measurement plan is written before them.
Every version, release date, download figure, star count and license below was read from a primary source on 2026-08-29: the PyPI JSON API, pypistats.org last-week counts, the npm registry with api.npmjs.org counts, and the GitHub REST API.
The boundary#
This completes a trilogy. Three kinds of tool read your source code and object to it, and they are distinguished by what they need to know in order to object.
| Needs | Objects when | Surveyed in | |
|---|---|---|---|
| Formatter | the grammar | never — it rewrites | 1.253 |
| Linter | the syntax tree | a pattern looks wrong | 1.250 |
| Type checker | a type system | a value’s type is wrong for its use | here |
A linter can tell you a variable is unused, because that is answerable from the shape of
the code. It cannot tell you that total is a string and you are subtracting from it,
because answering that means knowing what total is — which means tracking types through
every assignment, call and return in the program.
That is the whole difference, and it is the reason type checkers are slow, are usually a separate step, and fail programs a linter passes cleanly.
The hard case: checking at runtime instead#
beartype and typeguard outrank every static checker here on downloads — 14.9M and
13.4M weekly against ty’s 8.6M and pyright’s 8.7M. They are not static type checkers. They
read the same type annotations, but they enforce them while the program runs, raising
an exception when a function actually receives the wrong thing.
That is a different answer to the same need, and the trade is legible:
A static checker finds the bug without running the code, catches every path including the ones your tests never take, and can be wrong about dynamic code it cannot follow.
>A runtime checker finds the bug only on paths that actually execute, costs time in production, and is never wrong about what it saw — because it saw it.
They are in scope here, in their own section, because a reader choosing how to enforce types deserves to know both exist. They are not compared on speed with the static tools: they are not doing the same job, and a ratio between them would measure nothing. That is the same rule 1.250 applied to Pylint.
Also out of scope#
Type stubs and the typing runtime — typing-extensions at 350M weekly downloads is the
most-downloaded package in this whole neighborhood, and it is not a checker. It is the
library that lets you write the annotations the checkers read. Infrastructure, not a
choice.
Linters (1.250) and formatters (1.253), except where a linter’s rules need a type checker in the loop — the boundary case that is discussed rather than measured.
Candidates carried into S2#
Python: mypy, pyright, ty, basedpyright, pytype, pyre-check JavaScript/TypeScript: TypeScript, Flow Runtime enforcement: beartype, typeguard
Two are here to be written about rather than recommended. pyre-check is archived — the
GitHub API reports archived: true, which is Meta having shut it down, and it is included
because a dead tool with a clear cause of death is worth one paragraph. pytype last
released 2024-10-11 and carries 5 open issues.
Flow#
What it is: Meta’s static type system for JavaScript, and TypeScript’s only real historical competitor. Written in OCaml.
Read 2026-08-29: npm 0.329.0, released 2026-08-22. 450,351 weekly downloads. facebook/flow 22,281 GitHub stars, pushed 2026-08-29, 524 open issues. MIT.
The number that settles it#
450,351 against TypeScript’s 274,198,210. Roughly six hundred to one.
Flow is not dead — it shipped a week before this reading and the repository was pushed the day of it. It is maintained, actively, by a company that uses it at enormous scale. It is simply no longer a choice anyone outside that company makes.
This is a different shape from the two dead tools in this survey. pyre-check is archived and pytype has not shipped in 22 months; Flow is healthy and unchosen. A survey should not report those three the same way, because the risk to a reader differs: Flow will keep working, and nobody will be able to help you with it.
Why it lost, briefly#
Not on capability. Flow’s type system was in several respects ahead of TypeScript’s, and its soundness goals were more ambitious.
It lost on ecosystem. TypeScript shipped type definitions for the JavaScript world — DefinitelyTyped, and then packages shipping their own — and once library types existed for one system and not the other, the outcome followed. That is the same dynamic 1.250 found protecting ESLint and this survey finds protecting mypy: the durable advantage in every one of these categories is an ecosystem the competitor cannot reproduce, and it is not a property of the tool at all.
First read#
Do not start a project on Flow. Nothing else in this survey has a comparable ratio against its leader, and there is no persona in S3 for whom it is the answer.
Keep it if you already have it and it works — it is maintained, and a migration to TypeScript on a large Flow codebase is real work with no functional payoff beyond ecosystem access. That payoff is large, but it is not urgent, and “our type checker is unfashionable” is not a defect.
mypy#
What it is: the original Python type checker, from the people who designed Python’s type annotations. Reference implementation in practice, and the one against which “does this typecheck” usually means.
Read 2026-08-29: PyPI 2.3.1, released 2026-08-15. 38,833,853 weekly downloads. python/mypy 20,617 GitHub stars, pushed 2026-08-29, 3,215 open issues. MIT (PyPI; GitHub reports NOASSERTION, which reflects an unrecognised license file rather than an unlicensed project).
The position#
38.8M weekly is roughly four and a half times pyright’s 8.7M and four times ty’s 8.6M. In a category where a Rust challenger and a Microsoft entrant both have real adoption, mypy is still where most of the volume is.
Some of that is inertia and some is structural: mypy is what the typeshed ecosystem and
most CI configurations were written against, and it is the implementation the annotation
PEPs were prototyped in. mypy-extensions alone carries 63.5M weekly downloads as a
transitive dependency.
3,215 open issues against 20,617 stars is the widest tracker in this survey. Read alongside a repository pushed the day of reading and a release two weeks earlier, that is a large and actively-worked surface rather than neglect — the same reading 1.250 applied to Pylint’s 1,081.
What it is good at#
Gradual typing, which it was designed for. mypy’s premise is that you add annotations
to an untyped codebase a module at a time, and everything it does supports that: per-module
strictness settings, # type: ignore with error codes, and a strictness ladder a team can
climb over quarters rather than in one commit.
That is the opposite of a checker designed for a codebase that was typed from the start, and it is why mypy tends to win on codebases with history.
Plugins. mypy has a plugin interface, and the frameworks that bend Python’s semantics use it — Django’s ORM, SQLAlchemy, Pydantic and attrs all ship or rely on plugins that teach mypy what their metaclass and decorator machinery actually produces. Without that a checker either can’t see those types or is confidently wrong about them.
This is the same structural moat 1.250 found protecting Flake8 and ESLint, and it is worth naming as a pattern rather than a coincidence: in every one of these categories the incumbent’s durable advantage is an extension point the fast rewrite cannot reproduce.
Where it is weak#
Slow, and slow in the way that hurts — a cold full-project run on a large codebase is minutes, not seconds. The incremental cache helps in the editor and does not help CI.
Its inference is more conservative than pyright’s, which produces a familiar disagreement: code that pyright accepts and mypy rejects, or vice versa, on the same annotations. Two checkers implementing the same PEPs do not agree at the edges, and a team running both discovers this quickly.
First read#
Still the default for Python, particularly on a codebase adopting types gradually or one depending on Django, SQLAlchemy or Pydantic. The reason to look elsewhere is speed, and the reason to look carefully before moving is the plugin question.
pyright and basedpyright#
What pyright is: Microsoft’s Python type checker, written in TypeScript, and the engine underneath the Python experience in VS Code.
Read 2026-08-29: PyPI 1.1.411 (2026-06-25), 8,689,592 weekly downloads; npm 1.1.413 (2026-08-14), 891,614 weekly. microsoft/pyright 15,604 stars, pushed 2026-08-28, 323 open issues. MIT.
basedpyright: PyPI 1.39.10 (2026-08-13), 2,588,100 weekly downloads. DetachHead/basedpyright 3,562 stars, pushed 2026-08-21, 650 open issues.
Why a fork of Microsoft’s checker has 2.6 million weekly downloads#
basedpyright’s own README, read 2026-08-29, describes it as “a fork of pyright with various type checking improvements, pylance features and more.”
That phrase is the structural story of this tool. pyright is open source. Pylance is not. Pylance is Microsoft’s Python language server — the thing that provides the fuller editor experience — and it is licensed for use in official Microsoft builds of VS Code. Use VSCodium, or Neovim, or Emacs, and the open checker is available but the good editor integration is not.
basedpyright exists in the gap that creates, and 2.6M weekly downloads is the measure of how many people are standing in it.
This is a different openness question from the one 1.250 found, and worth keeping distinct. There, the issue was copyleft — Pylint’s GPL, which constrains a vendor distributing a product. Here the license on the checker is permissive and the constraint is on a proprietary layer above it. Same word, opposite shape: 1.250’s question was what you may do with the tool, this one is what the tool is allowed to do for you.
What pyright is good at#
Speed and inference, relative to mypy. It is generally the faster of the two on a cold run and its inference is more aggressive — it will conclude a type where mypy asks for an annotation.
Strict mode is strict, and it reports on unreachable code and unknown types in ways mypy does not by default. On a codebase typed from the start, pyright tends to be the more informative of the two.
It is the editor default for most Python developers, whether or not they chose it, because it is what VS Code ships.
Where it is weak#
No plugin interface. The Django, SQLAlchemy and Pydantic plugins that make those frameworks checkable under mypy have no pyright equivalent. Frameworks that bend Python’s semantics through metaclasses and decorators are where this shows, and it is the most common reason a codebase cannot simply switch.
Aggressive inference is a two-sided property. It means less annotation and more disagreement — code that mypy accepts and pyright rejects on the same annotations, at the edges where the PEPs are ambiguous.
Governance is Microsoft’s. The tool is MIT and the roadmap is not a community’s. The existence of a 2.6M-download fork is the evidence that this occasionally matters.
First read#
pyright if the codebase was typed from the start, if you want the editor and CI to agree by construction, and if you do not depend on a framework that needs a mypy plugin. basedpyright if you are not in official VS Code and want the Pylance-side features anyway — a real constituency rather than a curiosity, at 2.6M weekly.
pytype and pyre-check — the two that lost#
Included because a survey that lists only living tools leaves a reader wondering what happened to the ones they remember, and because the two deaths here have different causes and different lessons.
pyre-check — archived#
Read 2026-08-29: PyPI 0.10.0 released 2026-08-06; facebook/pyre-check 7,172 stars,
archived: true per the GitHub API, last pushed 2026-06-26. 43,431 weekly downloads.
MIT.
Meta’s Python type checker, written in OCaml and built for the scale of Meta’s own Python codebase. Its distinguishing capability was Pysa, a taint-tracking analysis that traced untrusted input to dangerous sinks across the codebase — the data-flow analysis that 1.250 records as absent from every open-source linter and present only in commercial SAST and Semgrep’s paid tiers.
Archived is unambiguous. Unlike the quiet-but-alive cases 1.250 had to distinguish — Flake8 at fourteen months and 23 open issues, still being kept — this is the maintainer saying the project is over. The 43K weekly downloads are a dependency graph draining, not a constituency.
What is worth carrying forward: the interesting thing pyre had was not type checking, at which it was one of several. It was Pysa. A capability that only ever existed inside a company-scale tool leaves with the tool, and nothing open-source replaced it.
pytype — quiet, and the numbers say so plainly#
Read 2026-08-29: PyPI 2024.10.11 — the version string is the release date, and it is 22 months old. google/pytype 5,032 stars, pushed 2026-03-16, 5 open issues, 183,187 weekly downloads. Apache-2.0.
Google’s checker, and architecturally the most distinctive one here: it inferred types for unannotated code rather than requiring annotations first. On a large untyped codebase that is a different proposition from mypy’s gradual-annotation model — it tells you what your code already means instead of asking you to say.
Read the signals together, because they point the same way. 1.250 established that low release cadence alone does not mean death — Flake8’s 23 open issues proved someone was still answering. Here the tracker is also nearly empty at 5 issues, but the last release is 22 months old and the last commit five months. An empty tracker on a live project means it is being kept; an empty tracker on a project that has not shipped in two years more likely means nobody is filing, because nobody is using it.
First read: do not start here. If whole-codebase inference on untyped Python is what you need, that gap is currently open — which is a finding about the category rather than a recommendation.
S1 Recommendation — Type Checkers#
First read from published evidence. No benchmarks behind any of it; the measurement plan is
../S2-comprehensive/measurement-plan.md.
JavaScript / TypeScript#
Use TypeScript. At 274.2M weekly downloads against Flow’s 450K there is no selection decision to make, and this survey should not manufacture one.
The live question is version 7. The compiler is now 88% Go and v7.0.2 shipped as stable. A rewrite of the implementation is not a change to the type system, and the claim is that the same code typechecks the same way, faster. That is a claim about a very large surface, and it is exactly what S2 measures — the same source checked by two implementations of one language spec is a before-and-after nobody can dismiss as marketing.
Until that measurement exists: pin the compiler version, and treat a 6→7 move as a change to review rather than a dependency bump.
Keep Flow only if you already have it. It is maintained and it works. A migration is real work whose payoff is ecosystem access, which is large but not urgent.
Python#
mypy if the codebase is being typed gradually, or if you use Django, SQLAlchemy or Pydantic. The plugin ecosystem is the deciding factor, not the speed. Those frameworks bend Python’s semantics through metaclasses and decorators, and without a plugin a checker is either blind to their types or confidently wrong about them. mypy has them; pyright and ty do not.
This is the same structural moat 1.250 found protecting Flake8 and ESLint, and naming it as a pattern is more useful than treating each instance as a coincidence: in every one of these categories the incumbent’s durable advantage is an extension point the fast rewrite cannot reproduce.
pyright if the codebase was typed from the start and you want the editor and CI to agree by construction — it is what VS Code runs, so for most Python developers it is already checking their code whether or not they chose it.
basedpyright if you are not in official VS Code. Pylance is licensed to Microsoft’s own builds; the fork exists to bring those features to VSCodium, Neovim and the rest. 2.6M weekly downloads is a real constituency, not a curiosity.
ty: try it, do not gate on it. 8.6M weekly downloads and 19,580 stars at version
0.0.75. 1.250 established what a 0.x contract means operationally — minor releases
change behavior and a floating version turns a green build red with no code change — and
the stakes are higher here, because a linter’s new rule is a warning and a checker’s changed
inference is a failed build. The download figure means many people are trying it, which is
what an Astral release attracts; it does not mean it is finished.
Do not start on pytype or pyre-check. pyre-check is archived. pytype has not released in 22 months.
Runtime enforcement, which is a different question#
beartype or typeguard at boundaries — deserialised JSON, config files, database rows, an LLM’s structured output. Anywhere the type is not knowable until runtime, a static checker cannot help by construction, and these can.
Not instead of a static checker. They catch disjoint sets of bugs: static catches the branch nobody ran, runtime catches the value nobody could have predicted. And do not decorate every function — the cost is real and belongs where the uncertainty is.
What S1 cannot settle, and S2 must#
- Is TypeScript 7 actually faster, and by how much? The one measurement in this survey with no confound: same source, same spec, two implementations.
- Where the Python checkers actually sit on speed, with a stated corpus. Every figure in circulation is a vendor’s or a repetition of one.
- How much the checkers disagree. Two tools implementing the same PEPs reject different programs, and the size of that disagreement is a migration cost nobody quotes. This is more useful than a benchmark and harder to produce.
- Whether ty can reach the frameworks. If mypy’s plugin ecosystem is the moat, ty’s answer to Django and SQLAlchemy decides whether it displaces mypy or merely joins it.
beartype and typeguard — checking at runtime instead#
What they are: libraries that read the same type annotations as the static checkers and
enforce them while the program runs. A function annotated to take a list[str] and
handed a list[int] raises, at the call, in the process.
Read 2026-08-29: beartype 0.22.9 (2025-12-13), 14,932,009 weekly downloads, MIT. typeguard 4.6.0 (2026-07-26), 13,395,203 weekly downloads, MIT.
They out-download every static checker except mypy#
14.9M and 13.4M weekly, against ty’s 8.6M and pyright’s 8.7M. Only mypy’s 38.8M is larger.
That ordering is the reason this section exists. A survey of “type checkers” that covered only the static ones would describe a category most of whose downloads it never mentioned — and would leave a reader believing the choice is mypy-or-pyright-or-ty when a large part of the ecosystem answered the question a different way.
The download figures come with a caveat that cuts both directions: both libraries are
frequently transitive dependencies rather than direct choices, so the number counts installs
rather than decisions. The same is true of mypy-extensions at 63.5M. Neither figure should
be read as “this many teams chose this”.
The trade, stated plainly#
Static checking finds the bug without running the code. It sees every path, including the ones your tests never take, and it can be confidently wrong about dynamic code it cannot follow. It costs build time.
>Runtime checking finds the bug only on paths that actually execute. It is never wrong about what it saw, because it saw it. It costs production time, and it turns a type error into an exception in front of a user rather than a message in CI.
The second row is the part teams underweight. A runtime type check that fires in production is a crash you introduced deliberately, and the argument for it is that a wrong type silently flowing onward is worse. That is a real argument and it is not automatically right.
Where each earns its place#
Validating data crossing a boundary. Deserialised JSON, a config file, a database row,
an LLM’s structured output — anything whose shape a static checker has to take on trust
because it is Any at the edge. Static checking cannot help here by construction; the type
is not knowable until runtime.
Codebases too dynamic to check statically. Where inference gives up, runtime enforcement still works.
As a complement, not a replacement. The common mature arrangement is a static checker in CI and runtime enforcement only at the boundaries — not decorating every function, which is where the performance cost becomes real.
What they are not#
Not a substitute for static checking, and adopting one does not remove the case for the other. They catch disjoint sets of bugs: static catches the branch nobody ran, runtime catches the value nobody could have predicted.
Not free. beartype’s design centers on keeping the per-call cost near-constant rather
than proportional to the data, which is the reason it can be left on; typeguard’s checks are
more thorough and correspondingly heavier. Anyone enabling either across a hot path should
measure it there rather than trusting a benchmark from elsewhere — including this survey,
which does not time them (see measurement-plan.md for why: they are not doing the static
checkers’ job, so a ratio against them would measure nothing).
First read#
Not an alternative to mypy, pyright or ty — a different tool for the boundary where those three are blind. Reach for one at deserialisation points and API edges. Do not decorate everything.
ty#
What it is: Astral’s Python type checker, written in Rust. The third act after Ruff and uv, and the first of the three to enter a category that already had a strong incumbent and a well-funded challenger.
Read 2026-08-29: PyPI 0.0.75, released 2026-08-26. 8,588,719 weekly downloads. astral-sh/ty 19,580 GitHub stars, pushed 2026-08-26, 897 open issues. MIT.
The two numbers that do not belong together#
Version 0.0.75. Nineteen and a half thousand stars. Eight and a half million weekly downloads.
That is adoption running far ahead of any promise the project has made about itself. ty’s star count is within a thousand of mypy’s 20,617 — a tool fifteen years older with four and a half times the downloads — and its weekly downloads are level with pyright’s, which is Microsoft’s and mature.
This is the Ruff pattern repeating, and 1.250 is the place to check it against: Ruff was
pre-1.0 four years in, and that survey’s operational finding was that a 0.x version
contract promises nothing, minor releases add behavior, and a floating version turns a
green build red with no code change. All of that applies here with more force, because a
type checker’s output is not advisory. A rule added to a linter produces a new warning; a
change in a checker’s inference produces a build failure on code that was fine.
At 0.0.75 the release cadence alone should set expectations: this is a project shipping
constantly and still choosing not to claim stability. Take that at face value.
What it is for#
Speed, and the same consolidation story Astral has told twice before. A Rust checker with a warm cache answers fast enough to run on every keystroke, and the pitch is a Python toolchain — ruff, uv, ty — that shares a parser and a mental model rather than three unrelated dependencies.
The open question, which S2 and S4 take up, is whether type checking is the same kind of problem as linting and formatting. Ruff won by being a faster implementation of a well-specified job with a legible migration path. Type checking has neither property: the “spec” is a set of PEPs with real ambiguity, the incumbents disagree with each other at the edges, and there is no rule-prefix mapping that makes a migration mechanical.
Where it is weak#
It is pre-alpha and says so. Anything written about its coverage or behavior is true for a version and a week.
Ecosystem plugins are the open question. mypy’s Django, SQLAlchemy and Pydantic plugins are what make those frameworks checkable at all. Whether ty can reproduce that — and whether it can do so without a plugin interface, which is exactly what Ruff’s architecture could not afford — is the thing to check before planning a migration.
Disagreement is not a bug you can wait out. Two checkers implementing the same PEPs diverge at the edges. Adopting a second one means adopting its opinions.
First read#
Worth watching closely and worth trying in a branch. Not worth making your CI gate depend on at 0.0.75, and the download figure is not a reason to — 8.6M weekly means many people are trying it, which is what you would expect of an Astral release, not that it is finished.
TypeScript#
What it is: the type system for JavaScript, and the compiler that checks it. Not one option among several — for practical purposes it is the only one, and the numbers say so.
Read 2026-08-29: npm 7.0.2, released 2026-07-08; the GitHub release v7.0.2 is tagged 2026-08-20. 274,198,210 npm downloads in the last week. microsoft/TypeScript 110,779 stars, pushed 2026-08-29, 5,171 open issues. Apache-2.0.
The finding: TypeScript 7 is the Go rewrite, and it has shipped#
microsoft/TypeScript now reports 88% Go through the GitHub languages API, with
TypeScript itself down to 10%. The v7.0.2 release notes point at
typescript-go as the tag’s origin and at Microsoft’s own “Announcing TypeScript 7.0” post.
This is the largest single event in this category and it is recent. The compiler that
essentially all typed JavaScript depends on was reimplemented in a different language and
released as stable. A preview channel, @typescript/native-preview, still carries 9.1M
weekly downloads of its own.
What it means for a reader is narrower than the drama suggests. A rewrite of the
implementation is not a change to the type system: the same code should typecheck the same
way, and the promise is that it does so faster. That promise is exactly the kind of claim
this survey exists to measure rather than repeat, and the measurement is unusually clean —
the same source, checked by two implementations of the same language spec, is a before and
after nobody can dismiss as vendor marketing. See S2-comprehensive/measurement-plan.md.
What it means for risk is that the version to pin is now load-bearing in a way it was not last year. A major-version compiler rewrite is where behavior differences hide, and “it typechecks the same” is a claim about a very large surface.
The position: not a choice, a substrate#
274.2M weekly downloads is not a market share, it is a floor. Flow, the only other JavaScript type system with a real history, sits at 450,351 — roughly six hundred to one.
That ratio changes what this survey can usefully say about JavaScript. There is no selection decision here for almost any reader. The real decisions are downstream of having chosen TypeScript: how strict to configure it, whether to run it in CI or only in the editor, and whether type-aware lint rules are worth their cost (1.250).
Where the cost actually lands#
tsc is not a linter and is not fast. It resolves types across the whole program,
including through node_modules, and that is the work. Two consequences show up in every
real project:
Type-aware lint rules inherit this cost. 1.250 recorded that rules like “do not await
a non-Promise” require the compiler in the loop, and that they are usually the dominant
cost in a TypeScript lint run. That cost is this tool.
Editor and CI are the same program doing different amounts. The editor keeps a warm program object and answers incrementally; CI starts cold every time. A team measuring “how slow is our typechecking” is usually measuring the cold path and reasoning about the warm one.
First read#
Not a recommendation, because there is nothing to recommend against. Use TypeScript. The questions worth energy are strictness settings, where in the pipeline it runs, and — new this year — whether to move to the 7.x Go implementation now or wait, which is a risk judgment about a rewrite rather than a choice between tools.
S2: Comprehensive
S2 Approach — Type Checkers#
S1 asked what exists. S2 asks how these things work — and in this category the question that explains everything is what the checker does when it cannot be sure.
The three questions S2 answers#
1. What does it do with uncertainty? Python and JavaScript are both dynamic, so every
checker constantly meets values whose type it cannot determine. What it does then — assume
Any and stay quiet, infer aggressively and risk being wrong, or demand an annotation —
is the single decision that produces most of the visible differences between these tools.
It is why two checkers implementing the same PEPs reject different programs.
2. Can it be taught? Frameworks bend the language. Django’s ORM, SQLAlchemy’s mapped attributes and Pydantic’s model construction all produce types no checker can derive from the source alone. A checker with a plugin interface can be told; one without is either blind or confidently wrong. This is the same structural question 1.250 asked about linters, and it has the same answer shape.
3. What does checking cost, and where does the cost land? Cold CI runs and warm editor sessions are the same program doing very different amounts of work, and teams routinely measure one while reasoning about the other.
What this pass does not do#
It does not rank them. The S1 finding — that the category splits by what the checker assumes about your codebase, not by quality — holds, and a recommendation needs a persona.
It does not claim they are interchangeable. Two checkers on the same annotated source disagree about what is an error. That disagreement is a migration cost nobody quotes and S2 treats it as a first-class property rather than a footnote.
It carries no measurements yet. measurement-plan.md explains what will be run, why the
TypeScript 6-vs-7 comparison is unusually clean, and why execution is deferred to a quiet
machine rather than taken on a contended one. Where S2 discusses cost it argues from
architecture and says so.
How a type checker actually works, and why they differ#
The common core#
Every static checker here does the same four things: parse the source, build a symbol table, resolve names to declarations, and then propagate types through the program until it either proves every use consistent or finds a use it cannot justify.
The interesting engineering is entirely in the fourth step, and specifically in what happens at the edges of what can be known.
Gradual typing: the design constraint everything else follows from#
Python and JavaScript were both dynamically typed for decades before they had type systems,
so neither checker could demand that every value be annotated. Both languages solved this
the same way, with a type that means “stop checking here” — Any in Python,
any in TypeScript.
Any is not a type in the ordinary sense. It is an escape hatch that is compatible with
everything in both directions, and it exists so that annotated and unannotated code can
coexist in one file. That single decision is why:
- adoption can be incremental — annotate a module, leave the rest, the checker copes;
- checking is unsound — an
Anycan carry a wrong value anywhere, silently, and no amount of checking downstream will catch it; - strictness is configurable — most of the settings in every checker here are, in
effect, arguments about how much
Anyto tolerate.
The unsoundness is not a defect anyone forgot to fix. It is the price of being adoptable in a language that already had a billion lines of untyped code, and every tool in this survey pays it.
The decision that separates mypy from pyright: what to do with an unannotated function#
mypy’s default is to leave it alone. An unannotated function body is not checked. The reasoning follows from gradual typing: if you have not said what this takes, you have not opted in, and reporting errors inside it would bury a team adopting types in noise from code they have not got to yet.
pyright’s default is to infer. It reads the body, works out what the parameters and return must be, and checks accordingly.
Everything readers notice downstream comes from this. pyright finds more in a codebase that was typed from the start, because inference reaches further. mypy is calmer on a codebase mid-adoption, because it stays quiet where you have not spoken. And the two disagree — code one accepts and the other rejects — most often exactly where inference had to guess.
Neither is wrong. They are tuned for different codebases, which is why S1 sorted this category by what the checker assumes about yours rather than by capability.
Why plugins exist, and why they are the moat#
A checker derives types from source. Frameworks produce types that are not in the source.
Django’s ORM builds model attributes through a metaclass at class-creation time. SQLAlchemy’s
declarative mapping turns a class attribute into a descriptor whose runtime type differs from
what is written. Pydantic synthesises __init__ from field declarations. In every case the
type a developer needs is real at runtime and absent from the text the checker reads.
A plugin bridges that. mypy has the interface and the ecosystem grew into it; pyright and ty do not have one.
This is the third time this pattern has appeared in this range, so name it rather than rediscover it: 1.250 found the extensible incumbents (Flake8, ESLint) holding off faster rewrites because rules compiled into a Rust binary cannot run a plugin you wrote. 1.253 found the same shape in formatting. Here it is again, and the consequence is the same: the incumbent’s durable advantage is an extension point, and speed does not substitute for it.
The open question for ty specifically is whether a Rust checker can offer one at all — Ruff’s architecture could not, for exactly the reason a plugin API means embedding an interpreter in the hot loop.
Where the time goes#
Three costs, in rough order:
Resolving dependencies. A checker must know the types of everything you import, which
means reading stubs or source for a large part of node_modules or site-packages. On many
projects this dominates, and it is why the first run is so much worse than the second.
Inference over the program graph. Propagating types is not a single pass; a change to one inferred type can force re-checking of everything downstream.
Cold versus warm. The editor keeps the program object alive and answers incrementally. CI starts from nothing every time. These differ by an order of magnitude, and a team that measures CI and reasons about the editor — or the reverse — will draw the wrong conclusion about which tool is fast.
The rewrite, structurally#
TypeScript 7 is the same checker reimplemented in Go (S1: the repository is now 88% Go, and
v7.0.2 is tagged from typescript-go). Two things follow, and they pull in opposite
directions.
In favor: Go gives real parallelism and no JIT warmup, and the dominant costs above — reading many files, propagating types across a large graph — are exactly the kind of work that parallelises.
Against, or at least worth checking: a reimplementation of a specification this large is
where behavioral differences hide. The promise is that the same code typechecks the same
way. That promise is checkable — the same source through both implementations, diagnostics
diffed — and measurement-plan.md treats it as the more valuable half of the experiment.
Configuration, side by side#
The minimal real configuration for each checker, and what the settings are actually
arguing about — which in every case is how much Any to tolerate.
mypy — the strictness ladder#
# mypy.ini, or [tool.mypy] in pyproject.toml
[mypy]
python_version = 3.12
warn_unused_configs = true
# The gradual-adoption mechanism, and mypy's whole design in four lines.
# Global defaults stay loose; modules opt in to strictness one at a time.
disallow_untyped_defs = false
[mypy-myapp.core.*]
# This package is done. Hold it to the full standard.
disallow_untyped_defs = true
disallow_any_generics = true
warn_return_any = true
[mypy-myapp.legacy.*]
# This one is not. Say so explicitly rather than letting it fail quietly.
ignore_errors = true
# Third-party packages with no stubs. Without this, every import is an error
# rather than a silently untyped value.
[mypy-untyped_vendor_lib.*]
ignore_missing_imports = true# Suppression carries an error code, so a blanket ignore cannot hide a second,
# unrelated failure that appears on the same line later.
result = legacy_api() # type: ignore[no-untyped-call]pyright — one setting that means the most#
// pyrightconfig.json, or [tool.pyright] in pyproject.toml
{
"include": ["src"],
"exclude": ["**/node_modules", "**/__pycache__", "src/legacy"],
"pythonVersion": "3.12",
// "off" | "basic" | "standard" | "strict". This one line is most of the
// configuration: strict turns on reporting for implicit Any, unknown member
// types and unreachable code, which is where the volume comes from.
"typeCheckingMode": "standard",
// Per-rule overrides sit beside the mode rather than replacing it.
"reportMissingTypeStubs": "warning",
"reportUnnecessaryIsInstance": "none"
}Note what is missing compared with mypy: no per-module strictness ladder. pyright has
exclude and per-rule severities, but not mypy’s “this package is strict and that one is
not” gradient. That is the gradual-versus-complete split from feature-comparison.md
showing up as a config format.
TypeScript — and the setting everything else hangs off#
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "nodenext",
// `strict` is eight flags at once, and strictNullChecks is the one that
// matters: without it, null and undefined inhabit every type, and most of
// what a type checker is for stops working.
"strict": true,
"noEmit": true, // checking only; a bundler emits
"skipLibCheck": true, // do not re-check node_modules' own .d.ts files
"allowJs": true,
"checkJs": false
},
"include": ["src"]
}skipLibCheck is worth understanding rather than copying: it skips checking the type
declaration files of dependencies. It is close to universal because it is a large speed
win and because a dependency’s own type errors are rarely actionable — but it does mean the
checker is trusting rather than verifying a large surface.
ty — pre-alpha, and configured like its siblings#
# pyproject.toml
[tool.ty.environment]
python-version = "3.12"
[tool.ty.rules]
possibly-unresolved-reference = "warn"The shape follows ruff’s: one table in pyproject.toml, rules by name. Anything written
about specific rule names here is true for a version and a week — this is 0.0.75.
Runtime enforcement — a decorator, not a config file#
from beartype import beartype
@beartype # checks on every call, at the boundary
def parse_rows(rows: list[dict[str, int]]) -> int:
return sum(r["n"] for r in rows)
parse_rows(json.load(f)) # raises here if the JSON is not that shapeThe static checkers cannot help on that last line: json.load returns Any, so every
downstream use typechecks by construction. That is the boundary case runtime enforcement
exists for, and the reason to put the decorator there rather than on every function.
Running them#
mypy src/ # exit 1 if it finds anything
mypy --strict src/ # every strictness flag at once
pyright # reads pyrightconfig.json / pyproject.toml
pyright --outputjson # for CI consumption
ty check src/
tsc --noEmit # check without emitting
tsc --noEmit --incremental # writes .tsbuildinfo; the warm pathtsc --noEmit is the CI invocation and --incremental is roughly what an editor does. They
differ by an order of magnitude on the same project, which is why a team measuring one and
reasoning about the other draws the wrong conclusion about which checker is slow.
Feature comparison — Type Checkers#
Architectural properties, not benchmarks. No timings exist yet; see measurement-plan.md.
The property that predicts the rest#
| Tool | Unannotated function body | Inference reach | Designed for |
|---|---|---|---|
| mypy | not checked by default | conservative | code being typed gradually |
| pyright | inferred and checked | aggressive | code typed from the start |
| ty | inferred | aggressive | (pre-alpha; not yet stable enough to characterise) |
| basedpyright | inferred and checked | aggressive, stricter defaults | pyright users outside VS Code |
| pytype | inferred whole-program | inferred types for untyped code | untyped codebases — effectively gone |
| TypeScript | inferred | aggressive | everything; there is no alternative |
Read the first column and most of the visible disagreement between mypy and pyright follows from it. A tool that stays quiet in unannotated code and a tool that infers into it will report different things about the same repository, and neither is malfunctioning.
Extensibility — the moat#
| Tool | Plugin interface | Framework support in practice |
|---|---|---|
| mypy | yes | Django, SQLAlchemy, Pydantic, attrs all have plugins |
| pyright | no | relies on the framework shipping its own stubs |
| ty | no | open question, and the one that decides its ceiling |
| TypeScript | no plugin for checking | DefinitelyTyped and packages shipping their own .d.ts |
| pytype | limited | — |
This is the third appearance of one pattern in the 1.250-259 range: the incumbent’s durable advantage is an extension point the faster rewrite cannot reproduce (1.250: Flake8 and ESLint; 1.253: the same shape in formatting). The exception is TypeScript, where the ecosystem solved it differently — libraries ship their own types, so no plugin interface was needed.
Openness — and it is not 1.250’s question#
| Tool | License | The catch |
|---|---|---|
| mypy, pyright, ty, basedpyright, Flow, pyre-check | MIT | — |
| pytype, TypeScript | Apache-2.0 | — |
| pyright | MIT | Pylance, the editor experience above it, is proprietary and licensed to official VS Code builds |
Every checker here is permissively licensed, so 1.250’s finding — that Pylint’s GPL is decisive for a vendor embedding a tool — has no analogue. The constraint in this category is a proprietary layer on top of an open core: pyright is MIT, but the fuller editor experience is not, and basedpyright’s 2.6M weekly downloads are the measure of who is standing in that gap.
Same word, opposite shape. 1.250’s question was what you may do with the tool. This one is what the tool is allowed to do for you.
Static versus runtime#
| Finds bugs on | Wrong when | Costs | |
|---|---|---|---|
| Static (mypy, pyright, ty, tsc) | every path, including untested ones | the code is too dynamic to follow | build time |
| Runtime (beartype, typeguard) | only paths that execute | never — it saw the value | production time |
They catch disjoint sets. Static catches the branch nobody ran; runtime catches the value
nobody could have predicted — deserialised JSON, a config file, an LLM’s structured output,
anywhere the type is Any at the boundary by construction.
Maintenance status, stated three different ways on purpose#
| Tool | Status | Evidence |
|---|---|---|
| TypeScript, mypy, pyright, ty, Flow | live | all pushed within days of 2026-08-29 |
| Flow | live but unchosen | 450K weekly against TypeScript’s 274.2M — six hundred to one |
| pytype | quiet, probably over | last release 2024-10-11 (22 months), 5 open issues |
| pyre-check | archived | archived: true per the GitHub API — the maintainer has said so |
1.250 established that low release cadence alone does not mean death, because Flake8’s 23 open issues proved someone was still answering. The distinction matters more here because three tools are in trouble in three different ways, and the risk to a reader differs in each: Flow will keep working and nobody can help you with it; pytype has probably stopped; pyre is definitely over.
Measurement plan (Step 3.5) — 1.251 Type Checkers#
Written before S2, per ADDING-RESEARCH.md. Decide what will be run, and how far up
docs/map/17-the-evidence-ladder.md this subject can go, before writing any analysis.
The one measurement worth taking#
TypeScript 6.0.3 against TypeScript 7.0.2 on identical source.
This is a cleaner experiment than anything 1.250 or 1.253 could run, and the reason is the reason matters. Every other comparison in this neighborhood has a confound: different tools implement different rule sets, make different trade-offs, and answer subtly different questions, which is why 1.250 had to construct matched rule sets before any ratio meant anything.
Here there is no confound. Same language specification, same source files, two implementations — 6.0.3 is the last TypeScript-in-TypeScript release (2026-04-16) and 7.0.2 is the Go rewrite (2026-07-08), both installable from npm. The output should be identical and only the time should differ. That is a before-and-after nobody can dismiss as vendor marketing, and it is the headline claim of the largest event in this category.
Secondary result, and possibly the more valuable one: do they agree? If the two implementations report different errors on the same source, that is a finding regardless of the timing — a rewrite promising “the same code typechecks the same way” is making a claim about a very large surface, and checking it costs nothing extra once both are installed. Diff the diagnostics, not just the clock.
The ladder#
| Level | Applies? | Covering |
|---|---|---|
repeated | Reject as evidence | Vendor speed claims for the Go rewrite. Recorded as claims to check. |
cited | Yes | Versions, downloads, stars, licenses, archived status — read from registries and the GitHub API on 2026-08-29, dated in S1. A registry API IS the primary source. |
measured-local | Target | tsc 6 vs tsc 7 wall time and diagnostic agreement. |
measured-browser | No | tsc is a Node program and the Python checkers are native or CPython. Nothing here runs in a reader’s browser. |
Corpus#
The same pinned corpus the other two surveys use — express 4.21.2 and axios 1.7.9
(lib/ only) — checked with --checkJs --noEmit. That keeps this comparable with 1.250
and 1.253 and keeps the corpus immutable and refetchable.
The limitation, stated rather than hidden: this is TypeScript checking JavaScript with
inferred types, not a TypeScript codebase with dense annotations. Real .ts source exercises
more of the checker. The 6-vs-7 ratio is still a valid comparison — both implementations do
the same work on the same input — but it should not be quoted as “how fast tsc is on a
TypeScript project”. If that number is wanted, it needs a pinned TypeScript corpus, and that
is a separate piece of work.
Not measured, and why#
- mypy against pyright against ty. They disagree about what is an error, so they are not doing identical work, and a wall-clock ratio would measure the disagreement as much as the speed. Constructing a matched configuration across three checkers is much harder than 1.250’s matched rule sets and is not attempted here.
- beartype and typeguard. Runtime enforcement is not the static checkers’ job. A ratio against them would measure nothing, the same reason 1.250 timed Pylint alone.
- Editor/incremental performance, which is what developers actually feel. It needs a warm program object and a keystroke workload, and this harness measures cold runs.
- Whether the checkers are CORRECT. Not a wall-clock question, and the more important one. Said plainly rather than implied by a benchmark.
Execution is deferred, deliberately#
This is not run yet, and the survey publishes without it.
Two reasons, both inherited from 1.250 and both recorded there:
- The machine is shared. 1.250 measured run-to-run spread tracking runtime almost exactly — Pylint 2.1%, ruff 36%, oxlint 77% — and established that below roughly 100 ms on a busy machine, wall-clock measures the scheduler. tsc runs take seconds, so this particular measurement sits comfortably above that floor, which makes it the first cell to run when the machine is quiet.
- One clean run should settle three things at once. 1.250 has two outstanding cells —
its JavaScript trio at nine repetitions, and the x86 architecture cell — and 1.253’s
architecture finding applies to this whole range. A single droplet run
(
run-droplet.sh, ~$0.10, under half an hour, measured at 2% spread) covers all of them. Doing this one alone on a busy laptop would produce a worse number for free rather than a good one cheaply.
Until it runs, S2 argues from architecture and registry evidence, and says so.
S2 Recommendation — what the architecture says#
No measurements yet — measurement-plan.md says what will be run and why it is deferred.
Everything here argues from design, and says so.
The decisions that actually bind#
Was your code typed from the start? That question, not speed, sorts the Python checkers. mypy leaves unannotated function bodies alone by default; pyright and ty infer into them. On a codebase mid-adoption mypy is calmer and pyright is noisy about work you have not done yet. On a codebase typed from day one pyright finds more, because inference reaches further. Neither is malfunctioning; they are tuned for different inputs.
Do your frameworks need a plugin? Django, SQLAlchemy and Pydantic produce types that are real at runtime and absent from the source a checker reads. mypy has a plugin interface and the ecosystem grew into it; pyright and ty have none. If you depend on those frameworks, that is the whole decision and no speed number touches it.
This is the third appearance of one pattern in this range — 1.250 found it protecting Flake8 and ESLint, 1.253 found the same shape in formatting — so state it as a rule rather than rediscover it: the incumbent’s durable advantage is an extension point the faster rewrite cannot reproduce.
Are you inside official VS Code? pyright is MIT; Pylance, the editor experience above it, is not. basedpyright’s 2.6M weekly downloads measure how many people that affects.
On TypeScript 7#
Pin the compiler version and treat a 6→7 move as a change to review. The type system did not change; the implementation was rewritten in Go. The claim is that the same code typechecks the same way, faster.
The speed half is plausible on architecture — the dominant costs are reading many files and
propagating types across a large graph, both of which parallelise, and Go gives real
parallelism with no JIT warmup. The sameness half is the one worth checking, because a
reimplementation of a specification this large is exactly where behavioral differences
hide. measurement-plan.md treats diffing the diagnostics as the more valuable half of the
experiment, and it costs nothing extra once both versions are installed.
On ty#
Try it; do not gate CI on it. 8.6M weekly downloads at version 0.0.75 means many people are looking, which is what an Astral release attracts — not that it is finished.
1.250 established what a 0.x contract means operationally: minor releases change
behavior, and a floating version turns a green build red with no code change. The stakes
are higher here. A linter’s new rule is a warning you can triage. A checker’s changed
inference is a failed build.
The question that decides ty’s ceiling is not speed, which it will win. It is whether a Rust checker can offer a plugin interface at all — Ruff’s architecture could not, for the same reason: a plugin API means embedding an interpreter in the hot loop, which costs most of what the rewrite bought. If ty cannot reach Django and SQLAlchemy, it joins mypy rather than replacing it.
What S2 could not settle#
- Any timing. Deferred to a quiet machine, with 1.250’s outstanding cells, so one clean run settles all of them. Publishing a contended number would be worse than publishing none.
- How much the checkers disagree on identical source. This is a migration cost nobody quotes and it is more useful than a benchmark — and harder to produce, because it needs a corpus annotated densely enough for the disagreement to surface.
- Editor performance, which is what developers actually feel. Cold CI runs and warm incremental sessions differ by an order of magnitude, and this harness measures cold.
S3: Need-Driven
S3 Approach — who needs a type checker, and why#
Six positions, ordered from the most common to the most specific. Each is somewhere a real person stands, and the answer changes between them — which is why this survey does not publish a ranking.
The question that sorts them is not which checker is best. It is what the checker has to assume about your codebase: whether it was typed from the beginning, whether your frameworks need a plugin, and whether the type is even knowable before the program runs.
That last one is the case people miss. At a boundary — deserialised JSON, a config file, an
LLM’s structured output — the value’s type is Any by construction, and no static checker
can help. It is a different tool’s job, and that is a persona rather than a footnote.
S3 Recommendation — by position#
| If you are… | Use | The deciding reason |
|---|---|---|
| Typing an untyped Python codebase | mypy, loose, ratcheting | Per-module strictness gradient; plugins for Django/SQLAlchemy/Pydantic |
| On a Python codebase typed from day one | pyright | Infers into unannotated bodies; editor and CI agree by construction |
| Not in official VS Code | basedpyright | Pylance is licensed to Microsoft’s builds; the fork closes that gap |
| On any TypeScript project | TypeScript — and diff 6 vs 7 yourself | There is no alternative at 600:1; the rewrite’s sameness is the risk |
| Receiving external data | beartype / typeguard at the boundary | Any means static checking cannot help there, by construction |
| Publishing a typed library | mypy and pyright in CI, plus py.typed | Your consumers do not all run your checker |
| Fighting slow typechecking | Measure first — usually not the checker | Dependency resolution and a discarded cache dominate |
The two questions that sort this category#
Was the code typed from the start? mypy stays quiet in unannotated bodies; pyright and ty infer into them. That single default produces most of the visible disagreement between them, and it means the right answer depends on your codebase’s history rather than on which tool is better.
Is the type knowable before the program runs? At a boundary — JSON, config, a database
row, an LLM’s output — it is Any by construction and no static checker can help. That is a
different tool’s job, and missing it is the most common way a team concludes type checking
“doesn’t work”.
What every persona agrees on#
- Pin the version. More sharply than in 1.250: a linter’s new rule is a warning, a
checker’s changed inference is a failed build. This applies to
tscmajors and to ty at 0.0.75 especially. - Check the plugin question before switching Python checkers. If Django, SQLAlchemy or Pydantic are in the stack, mypy has plugins and the alternatives do not. A faster checker that cannot see your ORM’s types is not an improvement.
- Static and runtime checking are complements, not alternatives. They catch disjoint sets of bugs.
- Do not loosen strictness for speed. That is a code-quality decision wearing a performance costume.
Persona: the data arriving is not what the types say#
Who: someone whose service reads JSON from an API, a config file at boot, rows from a
database, or structured output from an LLM. The static checker is green. Production says
NoneType has no attribute 'get'.
What they need: to understand why the checker did not catch it, and what does.
Why the checker was always going to miss this#
json.load() returns Any. So does an untyped ORM row, a yaml.safe_load, and any
response.json().
Any is the escape hatch gradual typing is built on: it is compatible with everything in
both directions, so every downstream use of that value typechecks by construction.
Annotating the variable dict[str, int] does not check anything — it asserts something,
and the checker believes you. Nothing verifies it, ever.
That is not a gap somebody forgot to close. It is the price of a type system that could be adopted by a language with a billion lines of untyped code already written, and every static checker in this survey pays it.
Recommendation: enforce at the boundary, at runtime#
beartype or typeguard on the functions that receive external data. They read the same annotations the static checker reads and check them while the program runs, so the failure happens at the boundary with the actual value, rather than four calls later with a confusing symptom.
At 14.9M and 13.4M weekly downloads these out-rank every static checker except mypy, which is itself the finding: a large part of the ecosystem answered this question a different way.
If the data is structured and you own the shape, a validating model is usually better still — Pydantic and its relatives parse into a typed object and reject at the edge, which gives both the runtime guarantee and a real static type downstream. That is a data-validation question rather than a type-checking one, so this survey points at it rather than covering it.
Where to put it, and where not to#
At deserialisation points and API handlers. That is where the type is unknowable statically, and where a wrong value does the most damage by travelling.
Not on every function. The cost is real and it buys nothing on internal calls the static checker already proved. beartype’s design keeps the per-call cost near-constant rather than proportional to the data, which is what makes leaving it on viable at all — but “viable” is not “free”, and a hot path deserves its own measurement.
The thing to understand#
Static and runtime checking catch disjoint sets of bugs. Static finds the branch nobody ran, including the one your tests never take. Runtime finds the value nobody could have predicted, because it is looking at it. Adopting one is not a reason to drop the other, and a team that treats them as alternatives will keep being surprised by whichever half they skipped.
Persona: not using official VS Code#
Who: a Python developer in Neovim, Emacs, Zed, Sublime, or VSCodium — the open-source build of VS Code without Microsoft’s proprietary additions.
What they need: the editor experience their VS Code colleagues have.
The gap is real and it is a licensing one#
pyright is open source. MIT, and it is the checker underneath Microsoft’s Python tooling. Anyone can install and run it.
Pylance is not. It is Microsoft’s Python language server — the fuller editor experience, built on pyright — and it is licensed for use in official Microsoft builds of VS Code. Not VSCodium, which is the same source without the branding, and not any other editor.
So a developer outside VS Code gets the checker and not the experience, and the boundary is a license rather than a technical limit.
Recommendation: basedpyright#
A fork of pyright that, in its own words, adds “various type checking improvements, pylance features and more” (README, read 2026-08-29). It exists specifically to close this gap, and it ships as a language server any LSP-capable editor can use.
2,588,100 weekly downloads is the measure of how many people are standing in this gap. It is not a curiosity fork — that is roughly a third of pyright’s own PyPI volume.
Trade-offs, stated: it is a fork, so it tracks upstream with a lag and its maintainer is one person rather than Microsoft. 650 open issues against 3,562 stars. For an individual developer’s editor that risk is small and reversible — the config is compatible, and moving back is uninstalling.
The alternative, if you would rather not depend on a fork#
mypy with its language server, or pyright itself as a plain LSP server without the Pylance features. Both work in any editor. You get checking without the fuller experience, which for many people is enough.
Why this is in the survey at all#
Because it is a different openness question from the one 1.250 found, and conflating them would mislead. There, the constraint was copyleft — Pylint’s GPL, which matters to a vendor distributing a product and to nobody else. Here every license is permissive and the constraint is a proprietary layer above an open core.
Same word, opposite shape: 1.250’s question was what you may do with the tool; this one is what the tool is allowed to do for you. A reader who learned the first lesson would draw the wrong conclusion here.
Persona: publishing a typed library#
Who: the maintainer of a package other people install. Users open issues saying their checker complains about your library, and different users report different complaints.
What they need: their types to work for consumers running a checker the maintainer does not use.
The problem is that consumers do not all run your checker#
Two checkers implementing the same PEPs reject different programs. mypy leaves unannotated bodies alone and infers conservatively; pyright infers into them aggressively. Code your CI accepts under one can produce errors under the other, in your users’ projects, reported as your bug.
For a library author this is not an edge case. It is the normal condition, and it is the reason to do something deliberate rather than pick a favorite.
What to do#
Ship a py.typed marker. Without it, consumers’ checkers treat your package as untyped
regardless of how carefully you annotated it — the annotations are simply not consulted.
This is the single most common reason a “typed” library is not.
Check with more than one. mypy and pyright in CI, on the public surface. It is cheap and it catches disagreement before a user does. If ty is stable by the time you read this, add it; at 0.0.75 it is a warning generator rather than a gate.
Be explicit at the boundary. Annotate public function signatures fully — do not rely on inference for anything a consumer touches. Inference is exactly where the checkers disagree, so an inferred public return type is a disagreement waiting to be reported to you.
Keep the public surface conservative. Elaborate generic signatures, overloads and protocols are where checker behavior diverges most. A simpler type that is right everywhere beats a precise one that only works under the checker you happen to run.
Testing the types themselves#
Annotations can be wrong in ways nothing catches: no test exercises them, because they do not run. mypy and pyright both support assertion mechanisms for this — checking that a given expression has the type you claim, and that code you expect to be rejected is rejected.
Treat that as part of the test suite for a library whose types are part of its interface.
What not to do#
Do not ship stubs separately if you can ship inline. A separate stubs package drifts from the implementation, and the drift surfaces as a user’s bug report.
Do not annotate for one checker’s benefit. If a signature needs a # type: ignore to
satisfy yours, consumers on a different checker inherit the underlying problem without the
suppression.
Persona: typechecking is the slow step#
Who: a team whose CI spends minutes in mypy or tsc, or whose editor lags on the
large files. Somebody has proposed switching checkers to fix it.
What they need: to find out what is actually slow before changing tools.
Measure first, and measure the right path#
Cold and warm are different programs. The editor keeps a program object alive and answers incrementally; CI starts from nothing every run. They differ by an order of magnitude on the same project. A team that measures CI and reasons about the editor — or the reverse — will change the wrong thing.
Decide which one hurts before touching anything.
The usual answer is not the checker#
Dependency resolution. A checker must know the types of everything you import, which
means reading stubs or source for a large part of site-packages or node_modules. On many
projects this dominates. In TypeScript, skipLibCheck: true stops it re-checking your
dependencies’ own declaration files, and it is close to universal for exactly this reason —
at the cost of trusting rather than verifying that surface.
Caching that is not working. mypy’s incremental cache and tsc --incremental both write
state that a fresh CI container throws away every run. Persisting that cache across runs is
usually a bigger win than any tool change, and it is a pipeline edit rather than a migration.
Scope. Checking the whole repository when only src matters. Cheap to fix, easy to miss.
Type-aware lint rules, if this is TypeScript. 1.250 records that these need the compiler
in the loop and are usually the dominant cost in a lint run. If both tsc and a type-aware
ESLint config are in the pipeline, the compiler’s work may be happening twice.
If it really is the checker#
TypeScript: the 7.x Go implementation is the answer the ecosystem is converging on. See the upgrade persona — diff the diagnostics before trusting the timing.
Python: pyright is generally faster than mypy on a cold run. ty will be faster than both and is at version 0.0.75, which is a genuine reason not to make CI depend on it yet.
Before switching, check the plugin question. If you use Django, SQLAlchemy or Pydantic, mypy has plugins and the alternatives do not — and a checker that is fast and cannot see your ORM’s types is not an improvement.
What not to do#
Do not loosen strictness to go faster. That is a decision about code quality dressed as a decision about performance. Make it on the merits, with the stopwatch put away.
Do not run two checkers to hedge. They disagree at the edges, and you will spend the saved time reconciling them.
Persona: deciding whether to move to TypeScript 7#
Who: anyone maintaining a TypeScript project, which is essentially every JavaScript
project of consequence. tsc is in CI, the editor is slow on the big files, and TypeScript 7
shipped in July.
What they need: to know whether this is a routine upgrade or a risk.
What actually changed#
The type system did not change. The implementation was rewritten in Go — the repository
is now 88% Go by the GitHub languages API, and v7.0.2 is tagged from typescript-go. The
promise is that the same code typechecks the same way, faster.
Both halves of that deserve different treatment.
The speed half is plausible on architecture. The dominant costs in a check are reading
the types of everything you import and propagating types across a large graph. Both
parallelise, and Go gives real parallelism with no JIT warmup. This survey has not measured
it — see ../S2-comprehensive/measurement-plan.md — and does not repeat the vendor figure.
The sameness half is the one to verify yourself, because a reimplementation of a specification this large is exactly where behavioral differences hide, and because it is cheap to check.
What to do#
Run both on your own codebase and diff the diagnostics. Install 6.0.3 and 7.0.2 side by
side, run tsc --noEmit under each, and compare the error lists. If they match, the upgrade
is a version bump. If they do not, you have found your migration work before it found you —
and this costs an afternoon, against a rewrite you did not do.
That is the same experiment this survey plans to run on a shared corpus, and your codebase is a better corpus than ours for your purposes.
Pin the version. A compiler major is not a dependency bump. Whatever you are on, be on it deliberately.
Do not upgrade the same week you change anything else. If diagnostics do shift, you want one variable in the diff.
What not to assume#
Do not assume “faster compiler” means “faster CI”. If your pipeline’s cost is
node_modules installation or the bundler, the checker was not the bottleneck. Measure
before attributing.
Do not assume type-aware lint rules get proportionally faster. 1.250 records that those rules require the compiler in the loop and are usually the dominant cost in a TypeScript lint run. They should benefit — but that is a chain of two tools, so measure it rather than infer it.
Flow users: none of this changes your position. Flow is maintained and unchosen at six hundred to one, and a migration’s payoff is ecosystem access, which is large and not urgent.
Persona: adding types to a Python codebase that has none#
Who: a team with a five-year-old Django or Flask service. No annotations, decent test
coverage, and a recurring class of bug where something is None that should not be.
What they need: to start getting value without a six-month annotation project.
Recommendation: mypy, loose, with a strictness ladder#
mypy is built for exactly this, and the thing that makes it the answer is the per-module
strictness gradient — global defaults stay permissive while individual packages opt in as
they are annotated. pyright has exclude and per-rule severities but not that gradient, and
on a codebase mid-adoption the difference is the whole experience.
The second reason is decisive if it applies at all: if you use Django, SQLAlchemy or Pydantic, mypy has the plugin and the others do not. Those frameworks build attributes through metaclasses and decorators, so the types a developer needs are real at runtime and absent from the source. Without a plugin a checker is blind to them or confidently wrong. That is not a preference, it is a capability the alternatives lack.
How to start, concretely#
Turn it on with almost everything off, and get a green build on day one. A checker that reports four thousand errors on its first run gets switched off in week two.
Ratchet, do not sprint. Annotate a package, mark it strict in the config, move on. The
[mypy-myapp.core.*] section pattern exists for this.
Type the boundaries first — function signatures at module edges, not local variables. That is where the value density is highest, because it is where wrong assumptions cross between people.
Expect ignore_missing_imports for untyped dependencies. Without it, every import of a
package with no stubs is an error rather than a silently untyped value, and the noise buries
everything real.
What they should not do#
Do not start with --strict. It is eight decisions at once on a codebase that has made
none of them.
Do not add ty yet. Version 0.0.75, no plugin story for the frameworks this persona depends on. Try it in a branch; do not stake the adoption effort on it.
Do not expect the checker to catch what the tests catch. Types find a different class of bug. A team adopting types to replace tests will be disappointed on both sides.
S4: Strategic
S4 Approach — what is still here in three years#
The trap in this category is that two of the four questions people ask are about companies: Microsoft’s commitment to pyright, Astral’s runway, Meta’s decision to walk away from pyre. A survey compares TOOLS, not the companies behind them. Funding and corporate backing answer will this survive; they never answer is this good.
So the questions here are narrower and checkable:
- Is anyone answering? Not release cadence — that measures how much a project’s scope is still moving. The signal is whether the tracker is being kept, and this survey has three tools in trouble in three distinguishable ways.
- What does it cost you if it stops? A type checker that dies leaves annotations behind, and annotations are portable in a way a config file is not. That changes the risk calculation, so this pass is precise about it.
- What is the governance? A foundation, a company, a fork, one person — four different profiles, and this category has all four.
- Where is the category going? Two forces are visible: a rewrite in a faster language that has already landed in JavaScript, and a consolidation attempt in Python that has not.
Every figure is from S1, read from primary sources on 2026-08-29.
S4 Recommendation — long-term view#
Safe to depend on for years#
TypeScript. 274.2M weekly downloads is infrastructure, not a dependency. Its one live risk is the Go rewrite, and that risk is bounded and checkable in an afternoon. The single-company concentration is real and has no practical alternative, which this survey states rather than works around.
mypy. 38.8M weekly, community governance under the Python organization, the plugin ecosystem the frameworks depend on. Its plausible future is a demotion in position — the checker you run for the plugins while something faster handles the inner loop — which threatens nobody depending on it.
pyright. Microsoft’s, tightly maintained at 323 open issues, and the engine under the editor most Python developers use. The asterisk is not viability: it is that a third of its volume has already routed to a fork because of a licensing decision above it.
Adopt with an exit in mind#
basedpyright. A fork with one maintainer, and an exit that costs nothing — the config is pyright-compatible, so leaving is uninstalling. That asymmetry is what makes 2.6M weekly downloads a reasonable position rather than a reckless one.
ty. Run it, do not gate on it. 0.0.75 is the number that governs, not the 8.6M weekly downloads or the 19,580 stars. What decides its future is not speed, which it will win — it is whether a Rust checker can offer a plugin interface at all. Watch that.
Keep if you have it, do not start#
Flow. Maintained, shipped a week before this reading, and unchosen at six hundred to one. The risk is support, not continuity: it will keep working, and nobody can help you. That argues for migrating when the isolation costs more than the migration would — a judgment about your team, not a deadline.
Do not start, at all#
pyre-check is archived; the maintainer has said it is over. pytype has not released in 22 months and has 5 open issues, which on a dormant project means nobody is filing rather than nothing is wrong.
The forecast, in one line each#
- TypeScript 7 becomes universal. The incumbent rewrote itself, so there is no migration politics — only the question of whether behavior is preserved.
- Python keeps two checkers. ty in the inner loop, mypy where the frameworks live. That is the same two-tier equilibrium 1.250 forecast for JavaScript linting, arriving here for the same reason.
- The extension point is the moat, for the third time in this range. Formatters, linters and now type checkers have each shown it. It is the most portable finding of the three surveys and it predicts the next instance before it happens.
- Two capabilities are simply gone — pytype’s inference for untyped code, pyre’s Pysa taint analysis — and nothing open-source replaced either.
Risk assessment#
Ordered by how likely a team is to hit it.
1. A checker upgrade fails the build with no code change — near certain#
Who: anyone floating a checker version, and especially anyone on ty.
Inference changes between releases. New inference finds new errors in unchanged code, and a type error fails a build rather than warning. This is sharper than the equivalent risk in 1.250 — a linter’s new rule is a warning you triage; a checker’s is a red pipeline.
ty at 0.0.75 makes no promise at all. tsc majors are where a rewrite’s behavioral
differences would surface.
Mitigation: pin the exact version in CI and in pre-commit, and treat a bump as a reviewed change. This is the single most common operational failure in the category and it is entirely preventable.
2. Switching checkers and hitting the plugin wall — common#
Who: Python teams moving off mypy for speed.
Django, SQLAlchemy and Pydantic produce types that exist at runtime and not in the source. mypy has plugins for them; pyright and ty have none. A team discovers mid-migration that its ORM is invisible to the new checker.
Mitigation: audit framework dependencies before benchmarking. It decides the migration; speed does not.
3. Believing the boundary is typed when it is Any — common, and it reaches production#
Who: anyone deserialising JSON, reading config, or consuming an LLM’s structured output.
Annotating data: dict[str, int] after json.load() does not check anything. It asserts,
and the checker believes you. Every downstream use then typechecks by construction, and the
failure appears far from its cause.
Mitigation: runtime enforcement at the boundary (beartype, typeguard) or a validating model. Understand that the static checker was never going to catch this and is not malfunctioning.
4. Two checkers disagreeing, in a consumer’s project — common for library authors#
Your CI is green under mypy; a user runs pyright and reports your library as broken. The checkers disagree most exactly where inference had to guess, which is unannotated and generic code.
Mitigation: ship py.typed, annotate the public surface fully rather than relying on
inference, check with both in CI, and keep public signatures conservative.
5. Measuring the wrong path when typechecking is slow — common#
Cold CI runs and warm incremental editor sessions differ by an order of magnitude. Teams measure one and reason about the other, then change tools to fix a problem that was a discarded cache or unresolved dependency stubs.
Mitigation: decide which path hurts, then look at dependency resolution and cache persistence before considering a migration.
6. Assuming a rewrite preserves behavior — this year’s risk, and bounded#
TypeScript 7 reimplements a very large specification in Go. The type system did not change; the implementation did, and that is where differences hide.
Mitigation: unusually cheap. Install 6.0.3 and 7.0.2, run both with --noEmit, diff the
diagnostics. An afternoon converts the unknown into a list.
7. Adopting a pre-alpha as a gate — rarer, self-inflicted#
ty at 8.6M weekly downloads and 19,580 stars looks like a mature tool by every signal except the one that governs: its version. 1.250 found the same pattern with Ruff and the consequence here is worse, because the output is a gate rather than a warning.
Mitigation: run it, do not gate on it. Adoption numbers are not a stability contract.
8. Depending on a proprietary layer you did not notice — rare, and specific#
pyright is MIT; Pylance is not, and is licensed to official VS Code builds. A team standardising on “pyright” while half of it uses VSCodium or Neovim will find the experience is not the same tool.
Mitigation: know which you are actually depending on. basedpyright exists for this and carries 2.6M weekly downloads.
9. Building on a capability that has already left — rare, and unfixable#
pyre’s Pysa taint analysis and pytype’s whole-codebase inference for untyped code both went away with their tools. Neither has an open-source replacement.
Mitigation: none available. If either is load-bearing for you, that is a commercial purchase or an internal project, and knowing it now is better than discovering it later.
Where this category is going#
The rewrite has landed in JavaScript and not in Python#
JavaScript’s is done. TypeScript 7 shipped as stable in July 2026; the repository is 88% Go. The compiler the whole ecosystem depends on was reimplemented in a faster language, by the same owner, with the same semantics as the goal. There was no competitive dynamic at all — no challenger displaced anyone, the incumbent rewrote itself.
Python’s is in progress and contested. ty is at 0.0.75 with 8.6M weekly downloads and 19,580 stars, against mypy’s 38.8M and 20,617. A different company is attempting to displace the incumbent rather than the incumbent rewriting itself.
The difference matters for the forecast. Microsoft did not have to solve migration, plugin compatibility or ecosystem trust — it is the ecosystem, so the rewrite ships and the ecosystem follows. Astral has to solve all three, in a category where the incumbent’s advantage is a plugin interface a Rust binary structurally struggles to offer.
So: expect TypeScript 7 to become universal and expect Python to keep two checkers. The likely equilibrium is ty in the inner loop and mypy where the frameworks live — which is exactly the two-tier arrangement 1.250 forecast for JavaScript linting, arriving in Python type checking for the same reason.
The pattern this range has now shown three times#
1.253 (formatters), 1.250 (linters) and this survey have each found the same shape:
the incumbent’s durable advantage is an extension point the faster rewrite cannot
reproduce. Flake8’s and ESLint’s plugins; mypy’s framework plugins; and in JavaScript,
TypeScript’s .d.ts ecosystem, which is the same idea solved by libraries shipping their own
types rather than by a plugin API.
Speed is a number that can be beaten. An ecosystem of things other people wrote and maintain is not, and the reason is architectural rather than commercial: running someone’s plugin means embedding their language’s runtime in your hot loop, which costs precisely what the rewrite bought.
This is the most portable finding in the range, and it predicts the next instance before it happens.
The gap nobody is filling#
Inference for wholly untyped code. pytype did this — told you what your unannotated code already meant, rather than asking you to say. It has not released in 22 months.
Nothing replaced it. mypy, pyright and ty all assume you will write annotations; they differ only in how aggressively they infer around what you wrote. For the very large amount of untyped Python still in production, the tool that met it on its own terms is gone.
And the same is true of taint analysis. pyre’s Pysa traced untrusted input to dangerous sinks across a codebase — the data-flow capability 1.250 recorded as absent from every open-source linter and present only in commercial SAST and Semgrep’s paid tiers. pyre is archived and Pysa went with it.
Two capabilities that existed only inside company-scale tools left when those tools did.
What would falsify this reading#
- ty shipping a plugin interface, or reaching Django and SQLAlchemy some other way. It would remove the structural reason to keep mypy and would mean Astral solved the problem Ruff could not.
- TypeScript 7 producing meaningfully different diagnostics from 6. It would turn a version bump into a migration and slow the transition considerably.
- mypy’s share actually falling. It has not; a forecast built on its stability should be rechecked against the registry rather than assumed.
Viability — TypeScript and Flow#
TypeScript#
Governance: Microsoft, Apache-2.0. One company, and the most widely depended-upon developer tool in this entire range.
Signals: 7.0.2 (npm 2026-07-08; the GitHub release tagged 2026-08-20), pushed 2026-08-29, 110,779 stars, 5,171 open issues, 274,198,210 weekly downloads.
Safe in a way nothing else here is. At 274.2M weekly it is not a dependency, it is infrastructure. The realistic risks are not about survival.
The live risk is the rewrite, and it is a one-time risk. The repository is now 88% Go and v7.0.2 shipped as stable. Reimplementing a specification of this size is where behavioral differences hide — not in the type system, which did not change, but in the thousand corners where the old implementation’s actual behavior was the specification.
That risk is bounded and checkable, which is what makes it manageable: install both, run both, diff the diagnostics. The persona in S3 says to do this on your own codebase, and the measurement plan does it on a shared corpus. Either way it converts an unknown into an afternoon.
The single-company question is real and has no practical answer. TypeScript is Microsoft’s and there is no alternative. Unlike pyright, where a fork routed around a licensing decision, nobody can fork TypeScript meaningfully — the value is the ecosystem of type definitions, and that follows the official implementation. Stating it plainly beats pretending the concentration is fine; it simply does not generate an action.
Flow#
Governance: Meta, MIT. 22,281 stars, pushed 2026-08-29, 524 open issues, 0.329.0 released 2026-08-22.
450,351 weekly downloads against TypeScript’s 274.2M — roughly six hundred to one.
Live, and unchosen. This is a third category that this survey needs and 1.250 did not: not archived like pyre-check, not dormant like pytype, but healthy and no longer selected. Flow shipped a week before this reading and its repository was pushed the day of it.
The risk profile is unusual and needs stating exactly, because a reader will otherwise map it onto the wrong shape. Flow will keep working. Meta uses it at enormous scale and has every reason to keep maintaining it. What a Flow user does not have is an ecosystem: no library ships Flow types any more, hiring is harder, and no answer to your problem exists on the internet.
That is a support risk, not a continuity risk, and it argues for a different response. Continuity risk says migrate before it breaks. Support risk says migrate when the isolation starts costing more than the migration would — which is a judgment about your team, not a deadline.
Why it lost is the pattern of this whole range. Not capability: Flow’s type system was in several respects ahead. It lost because TypeScript’s ecosystem shipped type definitions for the JavaScript world, and once library types existed for one system and not the other the outcome followed. That is the same dynamic protecting mypy’s plugins here and ESLint’s plugins in 1.250 — the durable advantage is an ecosystem the competitor cannot reproduce, and it is not a property of the tool at all.
Viability — the Python checkers#
mypy#
Governance: community project under the Python organization, with the closest thing this category has to standards-body legitimacy — it is where the annotation PEPs were prototyped.
Signals: 2.3.1 released 2026-08-15, repository pushed 2026-08-29, 20,617 stars, 3,215 open issues, 38.8M weekly downloads.
Safe. The volume is four and a half times its nearest competitor, the governance is not a
single company’s, and mypy-extensions alone sits at 63.5M weekly as a transitive dependency
across the ecosystem.
3,215 open issues is the widest tracker here. Against a repository pushed the day of reading, that reads as a large actively-worked surface rather than neglect — the reading 1.250 applied to Pylint’s 1,081.
Its risk is displacement in position, not death. If ty matures, mypy plausibly becomes the checker you run for the framework plugins while something faster handles the inner loop. That is a demotion, and it threatens nobody depending on it.
pyright#
Governance: Microsoft. One company, MIT license, and a roadmap that is not a community’s.
Signals: 1.1.411 (2026-06-25), pushed 2026-08-28, 15,604 stars, 323 open issues, 8.7M weekly PyPI downloads.
Safe, with one asterisk. 323 open issues on a project this widely used is a tightly-kept tracker. Microsoft ships it as the engine under its own Python tooling, so the incentive to maintain it is structural.
The asterisk is the fork. basedpyright exists at 2.6M weekly downloads because the checker is open and Pylance — the editor experience above it — is not. A third of pyright’s volume has routed around a licensing decision. That is not a viability risk to pyright; it is evidence that single-company control has already had a visible consequence, and that is worth knowing before depending on the roadmap.
ty#
Governance: Astral, venture-funded. MIT.
Signals: 0.0.75 released 2026-08-26, 19,580 stars, 897 open issues, 8.6M weekly.
The honest position: promising, and not yet a dependency.
The company question stays in its lane. Astral is venture-funded and has not shipped the paid product that implies. If it changed direction, ty is MIT with real adoption and would be forked — the realistic risk is stall, not disappearance. That is the same reading 1.250 gave Ruff.
The version is the operational risk and it is present tense. 0.0.75 promises nothing: minor releases change inference, and changed inference is a failed build on code nobody touched. 1.250 said pin Ruff’s version; here the same advice carries more weight, because a linter’s new rule is a warning you triage and a checker’s is a red pipeline.
What decides ty’s ceiling is not speed. It is whether a Rust checker can offer a plugin interface at all. Ruff’s could not — a plugin API means embedding an interpreter in the hot loop, which costs most of what the rewrite bought. If ty cannot reach Django and SQLAlchemy, it joins mypy in the toolchain rather than replacing it. Watch that, not the benchmarks.
basedpyright#
Governance: a fork, one primary maintainer. 3,562 stars, 650 open issues, pushed 2026-08-21, 2.6M weekly.
Adopt with an exit in mind — and the exit is cheap. Configuration is pyright-compatible, so leaving is uninstalling. That asymmetry is why 2.6M weekly downloads is a reasonable position rather than a reckless one: the commitment is small and reversible.
Its dependency is on upstream continuing to be open. If Microsoft changed pyright’s license, the fork’s position changes overnight — a low-probability risk worth naming because the entire reason the fork exists is a licensing boundary.
pytype and pyre-check#
pyre-check is archived — archived: true per the GitHub API. Not a judgment call. Meta
has ended it, and its distinguishing capability, the Pysa taint analysis, left with it and
was not replaced.
pytype has not released since 2024-10-11 — 22 months — with 5 open issues and a repository last pushed 2026-03-16. 1.250 established that low cadence alone does not mean death, because Flake8’s 23 open issues proved someone was answering. Here the tracker is also nearly empty and nothing has shipped in two years: on a live project an empty tracker means it is being kept; on a dormant one it more likely means nobody is filing.
Neither is a starting point. pytype’s whole-codebase inference for untyped code has no live equivalent, which is a gap in the category rather than a recommendation.