We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Structural Typing and the Hard Parts step 24 of 24
Writing a minimal .pyi for an untyped dependency
One untyped dependency at an architectural seam turns every value crossing
it into Any. --strict will not warn you, because the annotations on
your side are all present — they are just annotating values the checker has
given up on.
The escalation, cheapest first
-
types-<pkg>from typeshed, if it exists. -
mypy --install-types. -
Write your own stub — bootstrap with
stubgen -p pkg, then delete everything you do not use. -
follow_untyped_imports— infer from the untyped source. Better than nothing, worse than a stub. -
Per-module
ignore_missing_imports. Never global: the mypy docs describe the global form as “equivalent to adding a# type: ignoreto all unresolved imports”.
Step 3 is the one worth being good at, because a stub covering the twelve symbols you actually use is an afternoon’s work and permanently better than the alternatives.
Stub rules that a reviewer will check
-
Function bodies are
...on the same line as thedef. - No docstrings. A stub is not documentation.
-
No
from __future__ import annotations; stubs are never executed. - No quoted forward references — stubs are lazily evaluated by definition.
-
Include all
@overloadvariants and never the implementation. -
Use
_typeshed.Incomplete, notAny, for a symbol you have not typed yet. It reads as a to-do and greps cleanly. -
Declare
__all__if and only if the runtime module has one.
Your task
Emit the stub, minimally.
def used_names(consumer_source: str, module: str) -> set[str]
def solve(consumer_source: str, module: str,
signatures: dict[str, list[str]]) -> list[str]
used_names walks the consumer’s AST and returns exactly the attributes of
module it touches: names from from <module> import a, b (an aliased
import a as x still uses a; a bare * is ignored), plus every
<alias>.attr where <alias> came from import <module> or
import <module> as <alias>.
solve returns the stub as a list of lines. For each used name in sorted
order:
-
signatures[name]has one entry →f"def {name}{variant}: ..." -
signatures[name]has more than one →"@overload"then thedefline, for each variant, in the given order (overload order is semantic — do not sort it) -
the name is absent from
signatures, or maps to an empty list →f"{name}: Incomplete"
Prepend a header: "from _typeshed import Incomplete" if any Incomplete
was emitted, then "from typing import overload" if any overload was, then
a single "" separator line — but only if the body is non-empty.
A signature string is the text following the name, e.g.
"(path: str) -> list[list[str]]" or
"(sink: SupportsWrite, rows: list[list[str]]) -> int". Emit it verbatim:
a stub’s parameter may perfectly well be a Protocol, and that is exactly how
you type a duck-typed third-party argument you do not control.
The habit
Do not stub the whole library. Stub the seam. The stub is a contract you are
asserting about someone else’s code, and every symbol in it is a claim you
will have to keep true across their releases — so keep the claim small, and
let Incomplete mark the parts you have not verified.
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.