How do you give a compiled Python extension module type information with no source?
answer
- The binary has nothing to annotate
- A file beside the compiled module
- Introspection yields args and kwargs
- Nothing checks the contract for you
- Version the stub with the binary
basics
~20 sShip a hand-written .pyi stub beside the compiled binary inside the package, plus a py.typed marker so consumers' checkers use it. A checker resolves the extension module through the stub; the binary itself carries nothing a checker can read.
solid answer
~40 sA compiled extension module is a `.so` or `.pyd` binary with no Python source, so there is nowhere to put an inline annotation — stubs are the only option. Place a `.pyi` next to the binary inside the package (`flightdiff/_core.pyi` beside `flightdiff/_core.cpython-314-*.so`), add `py.typed` to the package, and declare both as package data so they reach the wheel. Generate a first draft with a stub generator that imports the module and introspects it, then fix it by hand: many C functions expose no signature at all, so `inspect.signature` fails on them and a generator can only emit `*args, **kwargs`. If you do not own the package, the same stubs can be published separately as a `<package>-stubs` distribution. Since nothing checks the stub against the binary, treat it as an API contract and regression-test it.
code
python · 5 linesimport sysconfig
suffix = sysconfig.get_config_var("EXT_SUFFIX")
print(f"flightdiff/_core{suffix}")
print("flightdiff/_core.pyi")go deeper
Know that a compiled module has no Python source, so its types can only come from a separate .pyi file shipped next to it inside the package.
Explain the full mechanism: the stub named for the module, the py.typed marker, both declared as package data, and why generated stubs from a binary come out as *args, **kwargs.
Demonstrate you have shipped one: version the stub with the binary, run an import-and-diff consistency check in CI, and back it with tests that exercise the annotated surface, because nothing else validates the contract.
Own the strategy — whether the compiled core is exposed directly or behind an annotated facade, who is on the hook for the stub, and how a wrong signature is prevented from reaching every downstream consumer at once.
### Why the usual answer does not apply An extension module is a shared library — `_core.cpython-314-darwin.so` on macOS, a `.pyd` on Windows — built from C, C++, Rust or a compiler-generated intermediate. There is no `.py` file to annotate, and a type checker will not (and cannot safely) load and execute a binary to introspect it. So the only way to describe such a module to a checker is a stub file, which is exactly the case stubs were designed for. ### The layout Inside the package, the stub sits beside the binary with the module's plain name: ``` flightdiff/__init__.py flightdiff/py.typed flightdiff/_core.cpython-314-darwin.so flightdiff/_core.pyi ``` A checker resolving `from flightdiff import _core` finds `_core.pyi` and uses it. Two packaging details do the real work: the stub and the marker are data files, so the build backend must be told to include them, and the stub's name must match the extension's *module* name, not its full platform-tagged filename. A common refinement is to keep the compiled module private and wrap it in an annotated Python facade — `flightdiff/api.py` with inline annotations, re-exporting from `_core`. That gives you annotations that are actually checked against calling code, and confines the hand-written stub to the narrow binary boundary. It costs a layer of indirection and, on a hot path, a Python-level call, which is why plenty of projects stub the extension directly instead. ### Generating the first draft, and why it is only a draft Stub generators ship with the major checkers; they import the module and walk its attributes to emit a skeleton. For an extension module the yield is poor, because introspection of C functions is thin. Unless the module was built with tooling that embeds signature metadata, `inspect.signature` raises `ValueError` for its functions, and the generator can only write `def compare(*args, **kwargs) -> Any: ...`. That stub type-checks everything and catches nothing. The generated file is a starting skeleton — the names, the classes, the module structure — and every signature is yours to fill in from the C source or the documentation. ### The contract problem This is the part senior interviews care about. Nothing verifies a hand-written stub against the binary it describes. The checker trusts the stub absolutely, so a wrong stub is worse than no stub: with no types, consumers get `Any` and no false confidence; with a wrong one, they get green builds around a call that fails at runtime. Consider a flight-schedule differ whose comparison core is compiled: the stub says `compare(left: bytes, right: bytes, *, strict: bool = ...)`, the C function was changed to require a timezone argument, and every caller still checks clean. Worse, a stub that is wrong about *which* inputs are accepted can turn a rollback into a partial-failure rollback — half the callers were rebuilt against the corrected stub and half were not, and both pass their checks. The defences are ordinary engineering ones. Version the stub with the binary in the same distribution so they cannot be installed apart. Run a stub-consistency tool that imports the built module and diffs its runtime attributes against the `.pyi`, catching removed names, renamed classes and arity changes even when it cannot see parameter types. And treat the stub as covered by tests: a regression pack — say the differ's 340 recorded cases — exercised through the annotated public surface will catch a signature that no longer matches, because those calls are both type-checked and executed. ### When you do not own the package If the extension belongs to someone else, you cannot add files to their installed package. Publish a stub-only distribution instead: an installable whose importable directory is named `<package>-stubs` and contains only `.pyi` files, mirroring the original's module structure. Checkers prefer that over the runtime package's own types, so it fills the gap without touching the dependency, and it can be released and rolled back on its own schedule. If your coverage is incomplete, put `partial` in its `py.typed` so checkers keep consulting the runtime package for the rest. ### What to say in an interview Name the mechanism (a `.pyi` in the package plus the marker), be honest that generation gets you a skeleton and not signatures, and spend most of your answer on the drift problem — versioning, an import-and-diff consistency check, and tests that exercise the surface the stub describes. That is the difference between knowing stubs exist and having shipped one.
- Why does auto-generating a stub from a compiled module produce so little useful typing?Because introspection of C functions is shallow. Unless the module was built with tooling that embeds signature metadata, the function objects expose no parameter information, `inspect.signature` raises `ValueError`, and a generator can only emit `*args: Any, **kwargs: Any`. You get the module's shape — names, classes, attributes — which is worth having, but every signature has to be written from the C source or the documentation.
- How would you detect that a stub has drifted from the extension it describes?Import the built module in CI and compare its runtime surface against the parsed `.pyi`: missing or extra module attributes, missing classes and methods, and where possible arity. That catches structural drift even though parameter types cannot be recovered. Pair it with tests that call the annotated public surface, so the checker and the interpreter both see the same calls, and pin the stub and the binary in one distribution so they cannot be installed at different versions.
- When is wrapping the extension in an annotated Python module better than stubbing it directly?When the surface is wide or unstable. A Python facade's annotations are checked against real code, so they cannot drift silently, and it lets you present a friendlier API than the C layer exposes while keeping the raw module private. You pay an extra call per invocation and one more layer to read, so on a hot, narrow interface a direct stub is usually the better trade.
saying these in an interview costs you the question
- Claims a checker can introspect the compiled binary
- Puts annotations in the C source expecting them to be read
- Names the stub after the platform-tagged filename
- Trusts a generated stub without reviewing signatures
- Ships the stub but forgets it in the built wheel
- Assumes a wrong stub is harmless because it is optional