
Inside Pylance's Type Server Protocol: Request Contracts, Indexing Boundaries, and Cache Semantics
Pylance has published a specification for a Type Server Protocol, and the interesting part isn't the announcement — it's the contracts the document has to get right. This post works through what a request must identify, what a response must say about itself, where indexing stops and type evaluation starts, how cache keys and invalidation rules have to behave, and where the whole model diverges from LSP's position-and-hover design. It also keeps a hard line between what is confirmed, what is inference, and what you should verify against the document yourself before writing a client.
On 2026-10-26, an aggregator headline said Pylance had published a specification for a Type Server Protocol: a standardized way for clients to query a Python type-analysis service, with type intelligence kept out of the editor. That headline and a one-line summary are the whole public record I have. No wire format, method table, version string, or worked example came with it.
That gap is the point. The question isn't whether Microsoft published a doc. It's whether the doc pins down what makes a protocol survive production: what a request must identify, what a response must say about itself, where indexing stops and evaluation starts, and how a client knows an answer has gone stale.
I'm treating the protocol as a contract to interrogate, not a feature to summarize. Some of this is grounded in what I can verify now. Some is inference. I'll keep those apart; a post about a spec you haven't read shouldn't read like a post about a spec you have.
Everything below that describes a specific method name, transport, or message field of the Type Server Protocol is a question or a template, not a report. The only confirmed claim I have is that a specification was published and that it separates type analysis from the editor frontend. Verify the rest against the document itself before writing a client against it.
On this page
What Pylance Actually Published: Confirmed Claims and Open Questions
The Confirmed Claims: A Spec, a Client/Type-Server Split, and Type Intelligence Outside the Editor
Strip the reporting down and three claims survive:
- Pylance has published a specification for a "Type Server Protocol."
- The protocol standardizes how a Python type-analysis service is queried by clients.
- The stated architectural goal is to separate type intelligence from the editor, opening the door to alternative frontends and independent implementations.
The background most readers already have: Pylance is Microsoft's Python language server for VS Code, and the analysis engine underneath it is Pyright, which is developed in the open at microsoft/pyright. Pylance itself ships as a proprietary extension, distributed through the VS Code Marketplace. The split between an open engine and a closed product distribution is the context that makes a protocol publication interesting — and also the thing that decides how much of it other editors can actually use. More on that at the end.
What the Public Reporting Doesn't Say: Transport, Framing, Message Shapes, and Versioning
Here is what I cannot currently answer, and what I refuse to guess:
| Question | Status |
|---|---|
| Transport (stdio, socket, HTTP, in-process) | Unverified |
| Framing (Content-Length headers, newline-delimited JSON, something else) | Unverified |
| Serialization (JSON-RPC 2.0, custom envelope, binary) | Unverified |
| Method naming and per-query shape | Unverified |
| Versioning strategy and negotiation | Unverified |
| Whether responses carry any staleness/provisionality signal | Unverified |
| Error model and cancellation | Unverified |
| Whether the spec is accompanied by a reference server you can run | Unverified |
If you take one thing from this post, take the table's shape: questions with an honest don't-know column. That's more useful than a confident summary, and it's what you'd actually write on a whiteboard before an integration.
Why a Type Server Protocol Exists at All
The Cost of an Editor-Embedded Type Checker: Process Lifetime, Memory, and Duplicated Plumbing
A Python type checker is not a linter with a nicer face. To answer a hover, it needs the types of the symbols in scope, which needs the imports resolved, which needs site-packages scanned, which needs the selected interpreter known. The dependency graph runs from a keystroke to a directory walk.
When that machinery lives inside the editor process boundary, every frontend pays for it separately:
- Process lifetime. Analysis state has to survive buffer edits but not editor crashes, so someone has to own a restart policy.
- Memory. A large workspace's symbol tables are measured in hundreds of megabytes to low gigabytes. Every window that spawns its own analyzer multiplies that.
- Duplicated plumbing. File watching, interpreter discovery, stub path resolution, config parsing, and the "should I re-analyze this file or everything that imports it" decision all have to exist somewhere. Today, each editor integration that wants good Python intelligence reimplements or vendors that logic.
A protocol is a way of saying: this plumbing should exist once, behind a boundary, with a documented contract. That's a genuinely good idea, and it's the same reasoning that produced LSP a decade earlier.
Where LSP Runs Out for Python: Whole-Workspace Inference, Stub Resolution, and Re-Analysis Scheduling
The Language Server Protocol specification is the right comparison point, and the comparison gets misstated a lot. LSP does not "not support" types — textDocument/hover returns typed signatures just fine. What LSP does not specify is everything around the analysis:
- Analysis configuration. LSP gives you
workspace/didChangeConfigurationwith an opaque payload. The spec does not care what your type checking mode is, so it cannot tell a client what belongs in a cache key. - Stub resolution. PEP 561 stub-only packages (PEP 561) change what a symbol means, but the protocol layer has no concept of "the answer changed because someone ran
pip install." - Re-analysis scheduling. LSP says
publishDiagnosticsis pushed. It does not say whether the server re-analyzed the file, its importers, or the whole workspace, or how long that took. - Provisionality. LSP has no vocabulary for "this answer was computed before your last edit landed."
That last one is the gap a type server protocol is obligated to close, and it's the one I'll spend the most words on. An LSP server that returns a slightly stale hover is a minor annoyance. A type server whose answers feed CI, code review, or a refactor tool is a correctness problem.
The Architectural Claim to Test: Querying Analysis vs. Moving Files Around
There are two things a "type server protocol" could be, and they are not equally interesting.
Version A — a document transport with typed answers. The client sends file contents and a position; the server returns a string. The server holds no meaningful cache contract with the client, and the client is responsible for knowing when to re-ask. This is LSP with better hover text. It's useful, but it is not a service boundary — it is a remote procedure call over a file buffer.
Version B — an analysis service with an explicit context. Requests identify the workspace, the interpreter, and the configuration; responses identify which revision of which file and which analysis configuration produced the answer, and whether that answer is final. The client can reason about staleness because the server told it something to reason with.
My position, stated plainly: only Version B is worth standardizing. A protocol that specifies method names but not answer provenance just moves the reimplementation problem from "write a type checker" to "write a cache-coherence layer," which is not obviously cheaper and is definitely more subtle. The first thing I would look for when reading the spec is whether the response envelope has room for the analysis context. If it doesn't, the rest is decoration.
The Request/Response Contract
What a Request Must Express: File Identity, Workspace Root, Interpreter, and Query Kind
Any workable request shape has to carry at minimum:
- File identity, not just a path. A URI alone is insufficient while the user has unsaved changes. You need either a content hash, a monotonic document version, or the full buffer inline. The distinction between "the file on disk" and "the document the user is looking at" is where most editor integrations produce their worst bugs.
- Workspace root(s). Multi-root workspaces are normal, and a symbol's meaning depends on which root's
pyproject.tomlapplies. - Interpreter selection. Which Python. Which
site-packages. Whether it's a venv, conda env, or system interpreter. Same code, different answers. - Analysis configuration. Type checking mode, strictness overrides,
python.analysis.*equivalents. If this isn't in the request (or derivable from a session the server owns), it cannot be in the cache key, and correctness quietly degrades. - Query kind and target. Hover at position, definition of symbol, diagnostics for file, full type of an expression — each is a different latency and a different cache granularity.
A request that carries only a path and a position is underdetermined. If the spec's request object is missing interpreter identity, that is a design flaw worth raising publicly rather than working around.
What a Response Must Carry: The Result, Its Analysis Context, and a Provisionality Signal
Three fields, minimum:
- The result.
- The context — resolved interpreter path and version, the analysis config identity, and the workspace revision the server believed it was answering against.
- A provisionality signal — a boolean, a revision comparison, or an enum like
final | stale | partial. Anything that lets a client decide whether to display, suppress, or re-request.
Without (2), the client cannot cache safely. Without (3), the client cannot decide whether a "cannot resolve import" diagnostic is real or an artifact of an index that is still warming. Every language server I have used has shipped at least one release where cold-start false positives in diagnostics were the top complaint. That's not a bug in the analyzer. It's a missing field in the protocol.
Capability Negotiation and Version Skew: Server-Declared, Per-Request, or Fixed by Version
There are three plausible models, and they impose very different client designs:
| Model | How it works | Client cost |
|---|---|---|
| Server-declared (LSP style) | initialize response lists supported features | Client must branch on a capability object; graceful degradation required |
| Per-request | Each request states required features | Verbose, but self-describing and easy to log |
| Fixed by protocol version | Version N supports exactly this method set | Simplest client; every additive change is a breaking change |
Whatever the spec chose, the version-skew question remains: what happens when a client built against protocol 1.2 talks to a server implementing 1.4? The good answer is explicit negotiation with a documented fallback. The bad answer is "the server ignores unknown fields," which sounds tolerant and produces silent partial behavior.
Ask before you write code: is there a handshake, and does it return a version, or only capabilities? A capability list without a version number is hard to debug in the field, because "the server didn't advertise it" and "the server is older than you think" look identical from the client side.
A Minimal Probing Harness: Learning the Wire Shape From Behavior, Not Assumption
If a runnable reference server ships alongside the spec, you can learn more in twenty minutes of probing than in an hour of reading prose. The harness below assumes JSON-RPC 2.0 bodies with LSP-style Content-Length framing — that framing is an assumption, not a confirmed fact, and the script fails loudly if the header is missing, which is what you want.
// probe.mjs — Node 20+. Run: node probe.mjs <server-binary> --stdio
const [cmd, ...args] = process.argv.slice(2);
const child = spawn(cmd, args, { stdio: ["pipe", "pipe", "inherit"] });
let buf = Buffer.alloc(0);
let nextId = 1;
const pending = new Map(); // id -> { t0, method, reqBytes }
function frame(obj) {
const body = JSON.stringify(obj);
return "Content-Length: " + Buffer.byteLength(body) + "\r\n\r\n" + body;
}
function request(method, params) {
const id = nextId++;
const t0 = process.hrtime.bigint();
const body = JSON.stringify(params);
child.stdin.write(frame({ jsonrpc: "2.0", id, method, params }));
pending.set(id, { t0, method, reqBytes: Buffer.byteLength(body) });
return id;
}
child.stdout.on("data", (chunk) => {
buf = Buffer.concat([buf, chunk]);
for (;;) {
const sep = buf.indexOf("\r\n\r\n");
if (sep === -1) return;
const header = buf.subarray(0, sep).toString("ascii");
const m = /content-length:\s*(\d+)/i.exec(header);
if (!m) {
console.error("unexpected framing, header was:", header);
process.exit(2);
}
const len = Number(m[1]);
if (buf.length < sep + 4 + len) return; // body not fully buffered
const body = buf.subarray(sep + 4, sep + 4 + len).toString("utf8");
buf = buf.subarray(sep + 4 + len);
onMessage(JSON.parse(body), Buffer.byteLength(body));
}
});
function onMessage(msg, resBytes) {
if (msg.id == null) {
console.log(JSON.stringify({ notification: msg.method, hasParams: msg.params != null }));
return;
}
const p = pending.get(msg.id);
pending.delete(msg.id);
if (!p) return;
const rttMs = Number(process.hrtime.bigint() - p.t0) / 1e6;
console.log(JSON.stringify({
method: p.method,
rttMs: Math.round(rttMs * 100) / 100,
reqBytes: p.reqBytes,
resBytes,
// what you actually want: the top-level keys of the response
resultKeys: Object.keys(msg.result ?? msg.error ?? {})
}));
}
request("initialize", { processId: process.pid, capabilities: {} });
The resultKeys line is the point of the exercise. It tells you whether the response is a bare value or an envelope with context, without requiring you to parse the spec correctly on the first pass. Log one line per request, run each query kind twenty times, and you have both the payload shape and a latency distribution.
For the cold/warm comparison, clear whatever server-side cache directory the spec defines and time a full run:
## <server-cache-dir> is whatever the spec documents — do not guess and delete
rm -rf <server-cache-dir>
/usr/bin/time -v node probe.mjs <server-binary> --stdio 2>&1 | tee run.log
The --stdio flag is also an assumption. If the server rejects it, that is your first real data point about transport.
The Indexing / Type Evaluation Boundary
Why the Split Matters: Indexing Is Workspace-Shaped, Evaluation Is Query-Shaped
These two workloads have almost nothing in common operationally.
Indexing is bulk, workspace-shaped, and amortizable. You can spend forty seconds on it if the next thousand queries each take three milliseconds. It wants parallelism, worker pools, and a persistent on-disk store.
Evaluation is a single query against an already-populated type environment. It wants to be under a hundred milliseconds, it wants to be cancellable the instant the user types another character, and it wants no I/O.
Conflating them is how you get an editor that freezes for thirty seconds on startup while a spinner says "indexing." A protocol that names the boundary explicitly lets clients do the right thing: show partial results during indexing, block on evaluation, and report the difference to the user honestly.
Scope Questions: Roots, Exclusions, Virtual Environments, and Third-Party Stubs
The single most common misconfiguration in Python tooling is indexing the virtual environment as first-party code. A .venv inside the project directory contains tens of thousands of files that are dependencies, not workspace sources. If the protocol doesn't have an explicit way to express "this subtree is a dependency environment, treat it differently," every client will reinvent the exclusion list, and some will get it wrong.
Concretely, the questions to answer from the spec:
- Are roots expressed as paths, URIs, or named workspaces?
- Is there an exclusion mechanism, or is it pure convention?
- Is the interpreter environment auto-discovered, or must the client name it?
- Are PEP 561 stub packages indexed eagerly, or resolved on demand at evaluation time?
- Does adding a new root require re-indexing everything, or is indexing per-root?
The stub question has real cost implications. Eagerly indexing every stub package in a large environment is expensive and mostly wasted; lazily resolving them makes the first query against a third-party type slow and unpredictable. Neither answer is wrong, but the spec should say which one it chose so clients can set timeouts accordingly.
Practical Check: Measure Cold-Index Time Against Time-to-First-Answer
A benchmark without conditions is a rumor. I haven't run this against the published server — the harness above is what I'd use, and the table below is a template, not results:
| Measurement | Why it matters | My value |
|---|---|---|
| Cold index wall time (cache cleared) | Upper bound on first useful answer; the CI cost | — |
| Time to first answer, cold | What a user actually feels on open | — |
| Time to first answer, warm | The steady-state number | — |
Median hover RTT, warm | Evaluation latency | — |
p95 hover RTT, warm | Tail behavior under load | — |
| Machine (CPU, RAM, disk type) | Index time is CPU and I/O bound | — |
| Repo size (files, Python LOC) | Scaling sanity check | — |
| Interpreter count in workspace | Multiplies cache keys, may multiply indexing | — |
Whether .venv files were indexed | The classic misconfiguration | — |
The interpretation rule I'd apply: if time-to-first-answer is within ~10% of cold-index time, indexing dominates and the protocol's cache semantics matter far less than its startup path. That flips the priority order for anyone integrating — you'd tune startup before you tune queries. If, on the other hand, warm hover RTT is in the tens of milliseconds while cold index is minutes, the cache is the whole product, and its invalidation rules become the thing you must understand before shipping.
Cache Semantics
What a Cache Key Must Include for Correctness: Content Hash, Interpreter, Config, and Version
A cache key that omits any of these is a correctness bug waiting for a CI run to expose it:
- File content hash. Not the modification time.
mtimelies constantly —git checkout,rsync --times, and container image layers all produce files with old timestamps and new content, or vice versa. A content hash is the only honest identity. - Resolved interpreter identity. Path plus version plus ABI tag. Two venvs with the same Python version but different installed packages give different answers to
reveal_typeon a third-party symbol. - Analysis configuration hash. Type checking mode and any per-path overrides. If you change strictness and get a cached answer, you have a wrong answer with a plausible shape — the worst kind.
- Protocol/analysis version. An engine upgrade that changes inference rules invalidates everything. If this isn't in the key, upgrades produce a cache poisoned with answers from the old analysis semantics.
That's four components minimum, and each one is a place a spec can be silent and a client can be subtly wrong.
Invalidation Triggers: File Change, Config Change, Dependency Install, Interpreter Switch
The triggers are easy to list and hard to implement:
| Trigger | Subtlety |
|---|---|
| File content change | Must invalidate the file and its importers, or at least their diagnostics |
| Config change | Whole-workspace invalidation; cheap to trigger, expensive to execute |
| Dependency install/remove | Silently changes symbol resolution; no file event necessarily fires |
| Interpreter switch | Complete invalidation; cache keys change wholesale |
| Server or protocol upgrade | Flush everything |
The dependency-install row is the one that bites. pip install requests in a terminal may or may not produce file-watch events the server sees, depending on how the environment is excluded from watching. If it's excluded — which is the correct default for performance — then the server has no idea the world changed. The protocol needs a way for the client to say "the environment changed, invalidate on my behalf," and I'll be looking for that in the spec.
The Dangerous Middle Ground: Cache Hits That Are Correct on Disk and Wrong in the Buffer
This is the failure mode worth designing against, and I want to be careful here: I have not confirmed that any specific implementation exhibits this behavior. It is the failure mode the architecture invites, and the one to test for.
The sequence:
1. models.py on disk: def parse(raw: str) -> Config:
2. buffer (unsaved) has: def parse(raw: str | bytes) -> Config:
3. hover on parse() in api.py
If the response is computed against the disk revision, the client displays (raw: str) -> Config for a call it is about to write with a bytes argument — and the user's next action is a type error the tool told them was fine. Nothing in the response was "wrong" from the server's point of view. The workspace snapshot was self-consistent. It just wasn't the snapshot the user was editing.
The only fix is in the contract: the response must identify which document revision it answered against, and the client must compare that to its own editor state. A boolean "provisional" is the minimum viable version. A revision stamp is better, because the client can decide how stale, not just whether.
Client-Side Discipline: Never Treat a Cached Answer as Authoritative
Rules I'd enforce in any client built on this, whatever the spec ends up saying:
- Always keep a force-re-evaluate path. A request parameter, a cache-bust sentinel, or a session reset. If the protocol gives you no way to bypass the cache, that is a blocker, not a nuisance.
- Distinguish display from decision. Showing a cached hover is fine. Auto-importing a symbol, applying a refactor, or failing a CI job on a cached diagnostic is not.
- Treat the type server as untrusted memory. Assume it can return an answer computed before your last save. Design the UX so that assumption is harmless.
- Log the context, not just the result. When a bug report says "hover showed the wrong signature," the only way to triage is the interpreter, config hash, and revision from the response.
How This Diverges From LSP's Hover-Only Model
Responsibility Table: Transport, Lifecycle, Workspace Model, Configuration, and Cache
| Concern | LSP (specified) | Type-server protocol (expected) |
|---|---|---|
| Transport | JSON-RPC over stdio or socket, Content-Length framing | Unverified |
| Lifecycle | initialize → initialized → requests → shutdown → exit | Unverified; likely similar |
| Workspace model | rootUri / workspaceFolders, file watching | Must additionally carry interpreter and analysis config |
| Analysis configuration | Opaque payload via workspace/didChangeConfiguration | Should be a first-class, hashable input |
| Cache | Out of scope for LSP; server-defined | Must be specified, or clients cannot reason about staleness |
| Query shape | Position-and-document scoped | Program-scoped: types, symbols, whole-workspace properties |
| Provisionality | No vocabulary for it | Required for correctness |
The middle rows are where the real design work is. LSP deliberately stays out of analysis configuration because it wants to be language-agnostic — correct for LSP, insufficient for a type server. A type server has to know what "strict mode" means, because it changes the answer.
The Key Difference: LSP Asks What's at This Position, a Type Server Asks What's True About the Program
LSP's verbs are locative: hover here, definition there, references at this symbol. They are answerable by a file-scoped analysis with modest cross-file reach.
A type server's questions are global properties of a program: what is the type of this expression under this interpreter and this configuration, which symbols are actually reachable, what breaks if this signature changes. Those questions can outlive any single document version, and their answers are only meaningful when paired with the context they were computed under.
That's why the response envelope matters more than the method list. A well-designed type server protocol is, at its core, a protocol for context-stamped answers.
Where the Two Coexist: LSP for Editing, the Server for Semantics, and the Coherence Problem
The obvious deployment is both protocols against the same workspace. LSP handles completion, formatting, rename, and the thousand small ergonomics that are already solved. The type server handles the questions LSP can't express.
The coherence problem is real. Two analyzers over one workspace means:
- Differing diagnostics. Users see hover types from one engine and squiggles from another, disagreeing about the same expression.
- Duplicated memory. Two symbol tables, two stub caches, two venv scans.
- Two staleness models. The LSP server may invalidate on save while the type server invalidates on change. Users can't tell which one is behind.
- Startup contention. Both index the same
site-packagesat the same moment on the same CPU.
The clean answer is for the LSP frontend to be a thin adapter over the type server — one analysis engine, one cache, two protocol surfaces. Whether the published architecture supports that is exactly the kind of thing to check in the spec, and exactly the kind of thing a spec can leave ambiguous.
Reading the Spec Yourself — Verification Checklist
Concrete Items to Confirm: Transport, Framing, Versioning, Error Model, Cancellation, Push vs. Pull
Work down this list and write the answer beside each item:
- Transport. stdio, unix socket, TCP, in-process? Who owns process lifetime?
- Framing.
Content-Lengthheaders, newline-delimited JSON, or something else? Does it handle multi-byte UTF-8 correctly (byte length vs. character count)? - Serialization. JSON-RPC 2.0 or a bespoke envelope? Are batches supported?
- Handshake. Is there an
initializeequivalent, and does it return a protocol version or only capabilities? - Versioning. Semantic version, integer, date? What is the compatibility promise for additive changes?
- Error model. Structured error codes, or a message and a number? Are "unknown symbol" and "index not ready" different errors? They must be.
- Cancellation. Is there a cancel message, or is it a timeout on the client side? For a query-shaped protocol, cancellation is not optional — a slow evaluation the user has moved past is pure waste.
- Push vs. pull. Do diagnostics arrive unsolicited, or must the client ask? If push, is there a way to say "don't send me provisional results"?
- Request identity. Does a request reference a document version, a content hash, or neither?
- Provisionality. Is there any field on the response that indicates staleness? If not, how does the spec expect clients to detect it?
Items 6, 7, and 10 are the ones I would fail a spec over. The rest are recoverable ergonomics.
Separating What the Spec States From What You Inferred
Spec-stated (verifiable by reading): The LSP 3.17 specification defines textDocument/hover as returning Hover | null, where Hover carries contents and an optional range. That is a factual claim I can point at in the specification. If I write it in a client, and it breaks, the spec was wrong or I misread it — both are checkable.
Inferred (not checkable from the artifact): A demo video shows completion working in an unfamiliar editor, and I conclude "the protocol supports completion with snippet support and a resolve phase." That conclusion requires that the demo used the protocol at all, that the editor implemented the full feature set, and that resolve exists. None of that is visible in footage. It is a plausible inference dressed up as a fact, and it is exactly the kind of claim that should be written as "the demo appears to show…" or dropped entirely.
Apply that test to every paragraph you write from the spec. If you can quote the line, it's a fact. If you can't, it's a question.
What Editor and Tooling Authors Could Build
The Non-VS Code Question: Licensing, Packaging, and Support Gates
The exciting reading of this announcement is "any editor can now have Pylance-grade Python intelligence." The honest reading is "any editor can now implement a client for a protocol whose reference implementation is distributed under Microsoft's terms."
Those are gated differently, and the gates matter more than the API:
- Licensing of the reference implementation. Pyright is open source. Pylance is a proprietary extension. A published protocol does not automatically make the reference server redistributable alongside Neovim or Emacs.
- Packaging and updates. VS Code extensions update themselves. A third-party editor has to solve distribution, versioning, and the "user has protocol 1.2 and server 1.4" problem for its own users.
- Support surface. Who debugs a Pylance-backed integration in an editor Microsoft doesn't ship? Historically, this is where enthusiasm meets a bug tracker.
None of these are reasons not to publish a protocol — they're the reasons the protocol document alone won't change the non-VS Code experience this year. Confirmed: a spec was published. Untested inference: that a usable open reference server ships with it.
Realistic Near-Term Builds: CI Type Reporting, Semantic Tooling, and Analysis Behind Code Review
The near-term value is not in new editors. It's in taking type analysis out of the editor:
- Structured CI type-check reporting. Today, most pipelines parse human-readable checker output and regex it into a report. A protocol with structured responses and explicit error codes turns that into a typed consumer. The win is real but small and immediate.
- Editor-agnostic semantic tooling. "Who calls this function, with types resolved" is a query grep cannot answer and a language model will guess at. A queryable analysis service makes that a subroutine instead of a research project.
- Analysis behind code review. Type-aware review comments — "this argument is
str | Noneat every call site" — require whole-program type knowledge and a stable answer. That's a batch job, not an interactive one, and it's a natural fit for a cache-heavy service.
All three are things a single team can build on top of a documented protocol without needing the protocol to be a ratified standard.
What Stays Out of Reach Until Both the Spec and an Implementation Are Stable
Be skeptical of:
- A second independent type server. Writing one is a multi-year effort. Until someone does, the spec is validated by exactly one codebase.
- Trusting cross-version compatibility. Version-skew behavior is unproven until there are actually two versions in the wild.
- Caching across machines. If the cache key includes an interpreter path, then a cache built on a developer's laptop and shipped to CI is a cache miss at best and a wrong answer at worst.
- Protocol-level refactoring support. Rename across files is an LSP problem that works today. A type server that also does refactors needs an edit-application model the spec likely doesn't have yet.
Open Questions and Risks
Single-Implementation Risk, Protocol Drift, and the Missing Second Server
The dominant risk with any single-vendor protocol is that the document describes the implementation rather than constraining it. Ambiguities in the prose are invisible when there is one implementation, because that implementation is the tiebreaker. They become visible when someone else reads the spec literally and diverges.
Three specific things I'd watch:
Cache semantics may be underspecified. If the spec describes request and response shapes but not invalidation rules, every client will implement its own staleness heuristic, and they will all be different. This is the most likely gap and the most consequential one.
Versioning may be an afterthought. Specs written alongside a shipping product often treat version negotiation as a future problem, because the reference client and reference server always match. It becomes a real problem on the first release after publication.
The provisionality story may be missing entirely. If responses are bare values, the protocol has no way to express "indexing, ask again" versus "resolved, this is final," and clients will paper over it with timeouts. Timeouts are how you get spinners that never resolve.
The historical parallel is instructive without being definitive. LSP became a real standard because multiple independent servers implemented it — pyright, jedi-language-server, and python-lsp-server all speak it — and their disagreements forced the spec to sharpen. A protocol with one implementation has not yet had that pressure applied.
Conclusion: A Spec With One Implementation Is a Documented API, Not a Standard
Why This Matters as a Service Boundary, Not as an Announcement
My position: the publication is worth more than the feature it describes, and less than it appears to promise.
It's worth something because it names a boundary that should have existed for years — type intelligence as a service, with a contract, instead of analysis plumbing reimplemented inside every editor. That boundary, done properly, is the difference between "Python tooling means VS Code" and "Python tooling means a service anyone can query."
It's worth less than it appears because a spec with one implementation is a documented API, not a standard. The things that make a type server protocol trustworthy are the unglamorous ones: cache keys that include the interpreter, responses that say which revision they answered against, an error code that distinguishes "not found" from "index not ready," and a version handshake that lets a 2027 client talk to a 2026 server without silently degrading.
If the published document nails those, the interesting consequences arrive in two or three years, when a second implementation exists and the spec has survived contact with a team that didn't write it. If it only nails the method names, we've moved the reimplementation problem from "write a type checker" to "write a cache-coherence layer" — which is not obviously progress, and is definitely harder to debug.
Until you've read the spec yourself, treat the type server as a fast cache you don't control: useful, worth adopting for CI reporting and review tooling, and never authoritative for anything that changes files or fails a build. Adopt it for the plumbing. Verify the contract before you trust the answers.
Further Reading
Primary Sources: The Spec, Pylance, LSP, and PEP 561
- The Type Server Protocol specification. The document that seeded this post. I'm deliberately not linking a URL for it, because the material I had did not include a canonical location and I will not invent one — search for the Pylance Type Server Protocol publication and read the primary document.
- The aggregator report that seeded this post: Pylance Publishes Type Server Protocol Spec for Python Analysis (news.lavx.hu) — headline and one-line summary only; treat as a pointer, not as a technical source.
- Pylance extension listing — VS Code Marketplace. For distribution terms and current feature set.
- Pyright — github.com/microsoft/pyright. The open analysis engine underneath Pylance; the best available reference for how the analysis itself behaves.
- Language Server Protocol 3.17 specification — microsoft.github.io. Authoritative for the comparison in this post, particularly
textDocument/hover,textDocument/publishDiagnostics, andworkspace/didChangeConfiguration. - JSON-RPC 2.0 specification — jsonrpc.org. Relevant if, and only if, the Type Server Protocol builds on it; verify first.
- PEP 561 — Distributing and Packaging Type Information — peps.python.org. Background for why stub resolution is a cache-key concern rather than a file-watching one.
- Python Typing Specification — typing.python.org. The normative description of the semantics a type server is answering questions about.
More posts

Agent-Native OS Design: Isolation, Permissions, and Audit Trails for Anolis OS 27
OpenAnolis’s Anolis OS 27 agent-native design reframes OS isolation, permissions, and audit trails around autonomous AI workers rather than human users.

Hardening Legacy PHP Against PHP2Shell When No Official Patch Exists
Harden legacy PHP deployments exposed to PHP2Shell with WAF rules, runtime restrictions, patch triage, and incident-response checks when no official fix is available.

Auditing Model Routing and Prompt Caching Before You Trust the Cost Chart
A hands-on audit framework for verifying that an LLM gateway's model routing and prompt caching actually cut token spend before you trust the vendor's cost chart.
Loading comments…