skip to content

What is typeshed, and where does a type checker get stdlib and third-party stubs?

level: middleimportance: should knowfreq 30%

answer

  1. Where a checker's stdlib knowledge comes from
  2. Declarations only, no runnable bodies
  3. One repo, two halves: stdlib and third-party
  4. PEP 561 fixes the lookup order
  5. A stubs package beats inline py.typed

basics

~20 s

typeshed is the central repository of .pyi type stubs. Type checkers vendor its standard-library stubs instead of reading CPython's source, and its third-party stubs ship as separate types-* distributions. An installed stubs package outranks a library's own inline annotations.

solid answer

~50 s

typeshed is the central repository of `.pyi` stubs. Its `stdlib/` half stubs the standard library and is vendored inside type checkers, so a checker reads typeshed rather than the installed CPython source; version differences live in `sys.version_info` branches inside the stubs and are resolved against the Python version you target, not the one running the tool. Its `stubs/` half covers third-party projects that ship no annotations, published as separate stub-only distributions named `types-<project>`. PEP 561 fixes the lookup order: the checker's own configured stub path, then an installed `<package>-stubs` distribution, then the package's inline annotations if it carries a `py.typed` marker, then typeshed's bundled stubs. A stubs package therefore outranks the library's own inline types — handy for overriding a bad annotation, and the usual cause of stale-stub errors once a library starts shipping `py.typed` itself.

code

python · 10 lines
python
# acmeutil.pyi - read by a checker, never imported at runtime
from collections.abc import Iterator

VERSION: str

def chunk(data: bytes, size: int = 4096) -> Iterator[bytes]: ...

class Session:
    timeout: float
    def close(self) -> None: ...

go deeper

for a junior

Be ready to say what a .pyi stub file is and that typeshed is where checkers get types for the standard library. Knowing that stubs hold signatures only, and are never executed, is enough at this level.

for a middle

Explain the mechanics: typeshed's stdlib stubs vendored in the checker, third-party stubs published as separate types- distributions, and the PEP 561 order that puts a stubs package ahead of a library's own inline annotations.

for a senior

An interviewer expects you to diagnose with this. Show how a stale stub pin produces errors against signatures the library no longer has, and why sys.version_info branches in stubs let you check 3.10 compatibility while running a newer interpreter.

for a principal

Own the policy question: whether your organization consumes community stubs, contributes fixes upstream, or maintains internal stubs for untyped dependencies, and how stub pins are versioned and reviewed so a stub never silently outranks a dependency's own types.

### What typeshed actually is typeshed is a single open-source repository of **type stubs**: `.pyi` files that carry declarations only, with `...` standing in for every function body. It has two halves that behave quite differently. `stdlib/` holds stubs for the standard library — every module, including the ones implemented in C. `stubs/` holds stubs for third-party projects that ship no annotations of their own; each lives in its own directory with a small metadata file, and each is published to the package index as a **separate stub-only distribution**, conventionally named `types-<project>`. ### Why the standard library is stubbed at all You might expect a checker to read type information out of the installed standard library. It does not. Large parts of CPython's Python-level source are unannotated, and modules implemented in C have no Python source to annotate in the first place. So a checker ships a vendored copy of typeshed's `stdlib/` stubs inside the tool and reads those instead. That has a useful consequence: the checker's model of the standard library is decoupled from the interpreter you happen to be running. Version differences are expressed *inside* the stub with `sys.version_info` branches, and the checker keeps the branch matching the Python version you ask it to target. You can run the tool under 3.14 while checking that your code stays valid on 3.10 — the stub, not the running interpreter, decides which names and signatures exist. ### The three ways a third-party package can be typed 1. **Inline** — the project annotates its own source and ships an empty `py.typed` marker inside the importable package, so a checker knows those annotations are meant to be consumed by callers. 2. **A stub-only distribution** — a separate installable distribution containing only `.pyi` files, in a directory named `<package>-stubs`. Nothing in it is ever imported at runtime; the `-stubs` suffix is the convention a checker looks for. 3. **typeshed's third-party stubs** — the `types-<project>` distributions described above, maintained by people who are usually not the library's authors. Note the vocabulary trap, and it is the one that costs people an afternoon: the installed *distribution* and the importable *package* deliberately have different names here. Installing a distribution called `types-acmeutil` drops a directory called `acmeutil-stubs` into site-packages, while the code you write still imports `acmeutil`. ### The precedence order PEP 561 fixes the order a checker searches, and it matters more than most people expect: 1. Stubs on the checker's own configured search path — a directory of hand-written stubs you point the tool at. (How you configure that path is the individual checker's business, not the language's.) 2. An installed `<package>-stubs` distribution. 3. The installed package's own inline annotations, if it contains a `py.typed` marker. 4. typeshed's bundled third-party stubs for that project. The load-bearing line is 2 before 3: **a stubs package beats the library's own inline annotations.** That is deliberate — it lets you override a wrong or incomplete upstream annotation without patching the dependency — but it also produces one of the most confusing failures in typed Python. A library adds annotations and a `py.typed` marker, typeshed removes and marks its stubs obsolete, but an old `types-*` pin is still in your lockfile; the stale stubs keep winning, and the checker reports errors against a signature the library abandoned two releases ago. The fix is to uninstall the stubs distribution, not to sprinkle ignores. ### Practical consequences Stubs version independently of the code they describe, so "the checker insists this attribute does not exist, but it plainly does at runtime" is nearly always a stub-freshness problem: the installed stub distribution is older than the installed library. Treat the stub pin as part of that dependency and bump the two together. Because a stub replaces the module's public interface *entirely* for the checker, anything missing from the stub is missing as far as the checker is concerned, even though it exists at runtime. A partial stub is a perfectly legitimate choice — it types the surface you actually use — but the gaps surface as `Any` and silence, not as errors, which is exactly the failure mode gradual typing is supposed to make visible. Everything here is a **static** artefact. No `.pyi` file is ever imported, `py.typed` is empty and exists only to be found, and a stub-only distribution installs no runtime code whatsoever. Python 3.14's deferred annotations (PEP 649) changed how annotations in real source are evaluated at runtime; they changed nothing about stubs, whose annotations were never evaluated in the first place. Finally, the two halves of typeshed have very different reliability profiles. The standard-library stubs are effectively canonical and evolve with each release. The third-party stubs are best-effort community work, and they are retired from typeshed once a project starts shipping its own types — which is the outcome you should want for any library you maintain.

  • A checker reports that a library attribute does not exist, but it plainly works at runtime. What do you check first?
    Stub freshness. The installed stub distribution versions independently of the library, so an older stub simply does not describe a newer attribute. Check whether the project now ships its own inline annotations with a `py.typed` marker: if it does, an installed `<package>-stubs` distribution still outranks them under PEP 561, and the fix is to uninstall the stale stubs rather than to add an ignore. Otherwise bump the stub pin alongside the library pin.
  • Why do typeshed's standard-library stubs use sys.version_info branches instead of one stub set per Python version?
    One set keeps a single source of truth and lets a checker analyze code for a Python version other than the one it runs under. The branches are evaluated statically against the target version, so a name added in 3.12 is visible when you target 3.12 and absent when you target 3.10 — which is exactly how you catch a call that would fail on your oldest supported interpreter.
  • A library you maintain starts shipping inline annotations with py.typed. What should happen to its typeshed stubs?
    They should be retired from typeshed and the `types-` distribution marked obsolete, because the authors' own annotations are now authoritative and will track the code. Until downstream users drop the old stub pin, those stubs keep winning under PEP 561 and mask your real signatures, so it is worth announcing the change in release notes as well as removing the stubs upstream.

typeshed is a card catalogue kept apart from the books: it describes what is on each shelf without holding a single page of the text. Slip a newer card into the drawer and readers go by that card — even when the book itself has since been rewritten.

saying these in an interview costs you the question

  • Thinks a checker reads annotations from the installed standard library
  • Believes .pyi stub files are imported at runtime
  • Says inline annotations always beat an installed stubs package
  • Confuses the types- distribution name with the importable package name
  • Assumes stubs update automatically when the library is upgraded
  • Thinks the py.typed marker file contains the type information

context