skip to content

What does the py.typed marker file in a Python package do?

level: juniorimportance: must knowfreq 45%

answer

  1. Types are opt-in, not automatic
  2. One small file inside the package
  3. PEP 561 marker
  4. Missing it means Any everywhere
  5. Must exist in site-packages, not just the repo

basics

~20 s

py.typed is an empty marker file shipped inside a package directory. Under PEP 561 it tells type checkers the package's inline annotations are meant to be used; without it, a checker ignores them and treats the package as untyped.

solid answer

~40 s

PEP 561 makes exposing types **opt-in**: a static type checker will not read a third-party package's inline annotations unless the installed package directory contains a `py.typed` file, right beside its `__init__.py`. The file is a marker, normally empty, and its presence alone is the signal — nothing reads its contents for an inline-typed package. When the marker is missing, the checker resolves the import to an untyped module: every attribute comes back as `Any`, and it reports the import as lacking types even though the source you can read on disk is fully annotated. The marker covers the whole package, submodules included, so one file per top-level importable package is enough. It has no runtime effect whatsoever — nothing imports it, and annotations behave exactly as they would without it.

code

python · 8 lines
python
import pathlib, tempfile

root = pathlib.Path(tempfile.mkdtemp())
pkg = root / "billing"
pkg.mkdir()
(pkg / "__init__.py").write_text("def total(cents: int) -> int:\n    return cents\n")
(pkg / "py.typed").write_text("")
print(sorted(p.name for p in pkg.iterdir()))

go deeper

for a junior

Recall the one-liner: an empty py.typed inside the package directory is what makes a library's annotations visible to type checkers, and without it they are ignored. Know that it does nothing at run time.

for a middle

Explain the mechanics: the marker must be present in the installed package under site-packages, one per top-level importable package, and its absence makes the module resolve to Any rather than raising errors. Be ready to say where the file goes relative to __init__.py.

for a senior

Show that you treat the marker as a public commitment: annotations become API once you ship it, so you check the library against itself before adding it, and you notice when a build drops the file and quietly untypes every consumer.

for a principal

Own the policy across many libraries: which internal packages are expected to ship types, how annotation coverage is gated in the build so the marker's promise stays true, and how you migrate consumers off ad-hoc suppressions once a dependency becomes typed.

### The problem the marker solves Annotations are ordinary Python syntax: any library can write `def total(cents: int) -> int` without promising anything about them. When third-party type checking became practical, the ecosystem needed a way to distinguish a library that has *deliberately* annotated its public surface and stands behind those annotations from one that has a handful of decorative hints, or annotations used for something else entirely (a serialization framework, a dependency injector, a documentation tool). PEP 561 settled that with an explicit opt-in: a distribution declares "my inline annotations are for type checkers" by shipping a file named `py.typed` inside the importable package. ### Where it goes Inside the package directory, next to `__init__.py`: ``` billing/ __init__.py invoices.py py.typed ``` Not at the repository root, not next to `pyproject.toml`, and not in the `.dist-info` metadata directory the installer creates. The checker looks for it where the package is *installed* — in the environment's `site-packages` — so it has to be part of the built artefact, not merely present in your source tree. That distinction is where most of the real-world failures come from: the file exists in the repo, the wheel does not contain it, and every consumer sees an untyped dependency. The file is conventionally empty. For an inline-typed package its mere presence is the whole message. (The one case where contents matter is a stub-only package, where a `py.typed` holding the word `partial` marks the stubs as incomplete.) A single marker covers the package and everything under it, so `billing/invoices.py` and `billing/tax/rates.py` are both covered by `billing/py.typed`. If one distribution installs two independent top-level packages, each needs its own marker. ### What happens without it A checker that resolves `import billing` and finds no stub package, no marker and no bundled typeshed entry has to decide what `billing` is. It treats the module as untyped: the import is reported as having no type information, and every name reached through it degrades to `Any`. `Any` is contagious — it silences errors rather than producing them — so the practical damage is not a wall of red, it is a quiet hole in coverage. Code that calls your library gets no checking at all, and a genuine mistake such as passing a `str` where the library wants an `int` sails through. Consumers can suppress the report on their side, but suppressing it does not give them types; it only hides the fact that they have none. ### What it does not do Nothing at runtime reads `py.typed`. It does not enable runtime validation, it does not make the interpreter enforce annotations, and it does not change how or when annotations are evaluated. It is a static-analysis signal that happens to be delivered through the packaging system, which is why it is a packaging concern rather than a typing-syntax one: the decision is *shipped*, and it can be lost by the build. It is also a promise, not a proof. Adding the marker asserts that the annotations are intended to be consumed; if half your public functions are unannotated, a checker will infer `Any` for them and callers will silently get less checking than they think. The marker is therefore usually added at the point where a library starts checking itself as part of its own build, so that the promise stays true release over release. ### How a checker reaches the decision Resolution runs in a fixed order. A checker asked to resolve `import billing` looks first at stubs you have configured or vendored yourself, then for an installed `billing-stubs` package, then for `billing/py.typed` and the inline annotations it unlocks, and finally at the bundled stubs it ships for the standard library and for a curated set of third-party projects. The marker is one rung on that ladder, which explains two things people find surprising: a stub package can override your inline types even when you have shipped the marker, and a dependency can be fully typed for a consumer without shipping anything itself, if the checker already bundles stubs for it. ### The other direction A package you do not control cannot be marked by you. That is what a stub-only distribution is for: a separate distribution shipping `.pyi` files under a `<name>-stubs` directory, which a checker prefers over inline types. For your own library, though, the marker plus honest inline annotations is the cheaper and more durable answer, because the types live next to the implementation and cannot drift from it.

  • Does one py.typed cover submodules, and what about a distribution that installs two top-level packages?
    One marker covers the package it lives in and everything beneath it, so submodules and subpackages need no marker of their own. Coverage is per importable top-level package, not per distribution: if one distribution installs `billing` and `billing_cli` as two independent top-level packages, each directory needs its own `py.typed`. Namespace-package portions are the awkward case, since each portion is installed separately and each has to carry the marker.
  • Does adding py.typed change anything at run time?
    No. Nothing imports or reads the file at run time, and it neither enables validation nor alters annotation evaluation. It is purely a static signal consumed by type checkers while they analyse code, delivered through the packaging system because that is the only place a consumer's checker can reliably see it.
  • Should you add the marker to a library whose annotations are only half finished?
    Only if you are willing to treat the annotations as public API. The marker makes a checker trust what is there and infer `Any` for what is not, so partial coverage gives callers weaker checking than they assume while making mistakes look approved. The usual sequence is to annotate the public surface, check the library against itself in the build, then ship the marker and treat annotation changes as API changes.

It is the allergen label on a package: the ingredients are inside either way, but a shopper who scans for the label and does not find one has to treat the contents as unknown.

saying these in an interview costs you the question

  • Thinks a dependency's annotations are used automatically
  • Puts py.typed at the repository root instead of inside the package
  • Believes the marker enforces types at run time
  • Confuses the marker with a checker configuration setting
  • Assumes an installed package is typed because its source has annotations
  • Expects the file to need specific contents for an inline-typed package

context