September 10, 2017·4 min read·engineeringpythontypingpackaging

PEP 561 makes type information a packaging concern

The first PEP 561 draft proposes package metadata and a resolution order so type checkers can discover annotations and separately distributed stubs.

Writing a type annotation does not explain how another developer's type checker will find it. That becomes a packaging problem as soon as the annotated code is a dependency. A checker needs to locate the relevant information, decide whether an installed package intends to provide it, and distinguish that information from a local replacement. The first PEP 561 draft addresses those questions through package metadata and an explicit search order.

The initial draft was added to the PEP repository on September 10. Its creation field says September 9. The proposal is still a draft, and the design described here is that first public repository version. It proposes a typed argument to packaging setup, not a new runtime requirement for ordinary Python imports.

I find this problem more consequential than another convenient annotation syntax. Python's dependency ecosystem is a major reason to use the language, but discovering what those dependencies promise should not require an unrelated path configuration for each one. A distribution convention can make the information travel with the code or with an explicitly associated stub package.

Installed does not mean ready for checking

The rationale describes limitations of an earlier PEP 484 distribution recommendation, which uses a shared type-hints directory. Adding a separate path for each third-party library does not scale well. Simply exposing an entire installed-package directory also creates problems when type checkers encounter highly dynamic libraries. The draft names Django and SQLAlchemy as examples in that discussion.

The proposed opt-in is important because it separates availability for execution from an intention to supply usable type information. An installed package may execute correctly without presenting definitions that a checker can interpret as the author intends. Discovering every installed module indiscriminately would turn the dependency directory into an implicit claim that all of it is suitable input for static analysis.

The draft proposes querying package metadata through pkg_resources. A missing typed value tells a checker to skip that package. The mechanism gives maintainers a way to declare participation instead of making checkers guess from the presence of particular source syntax. It also means packaging tools and checker implementations must agree about how that declaration becomes accessible after installation.

The first design extends setup metadata

For inline annotations, the draft proposes typed="inline" in the distribution's setup() call. Its example also adds typing to setup_requires because the proposed distutils extension would live there. This is proposed configuration, not an instruction that an existing installation of the packaging tools already supports the new keyword.

That distinction follows from the state of the document. The PEP describes where the extension would be added and what a package author could then write. A draft can identify the intended integration point without having completed the implementation or established compatibility across the packaging tools people use.

Inline type information resides with the runtime implementation. A maintainer changing a callable can update its annotation in the same code. A separate stub file gives a different arrangement, with the interface described in a .pyi file. The draft prefers placing those files alongside their corresponding .py files and proposes typed="stubs" for a distribution providing them.

The draft also permits a subdirectory arrangement for stubs, using an inner package with the same name. It says that checking .pyi before .py remains the normal resolution rule. This is a concrete attempt to fit stubs into existing package lookup behavior. The directory layout matters because the checker must find the intended module while the runtime package continues to retain its own organization.

Third-party stubs have a separate owner

The proposal accounts for a library maintainer who does not want to maintain annotations or package stubs. Another party can distribute the type information. For a runtime library named flyingcircus, the initial draft names the stub package flyingcircus-stubs and places its stub files in a flyingcircus subdirectory.

Separating ownership allows typing support to develop without requiring every runtime-library author to adopt the same maintenance responsibility. It also creates a compatibility relationship. In this first draft, the stub package's version should match the runtime package's version, and a checker should verify that match. The requirement is stricter than merely finding a package with a plausible name.

That relationship is necessary because a stub describes an API that exists somewhere else. A checker accepting arguments according to an outdated stub has not established that the installed runtime supports those arguments. Conversely, a stub missing an available method can reject useful code. Distribution makes the information discoverable, while the version relationship gives the checker evidence about which implementation it describes.

Search order decides which description applies

The draft's search order starts with user code. It next considers stubs or source supplied through PYTHONPATH, providing a way to replace broken installed information. Third-party stub packages precede inline packages, and typeshed follows those sources. These are proposed precedence rules for type information, so their order has observable consequences for checker results.

A local replacement is useful when a dependency's definitions need correction before its next release. The checker must have an intentional route to consume that replacement, otherwise an installed copy can keep taking precedence. At the same time, the existence of an override means two environments with the same runtime packages can check against different interface descriptions. Understanding the search path becomes part of explaining a diagnostic.

I prefer an explicit precedence rule to an accidental dependency on whichever directory happens to be searched first. The proposal makes that rule discussable and gives library authors a way to reason about how their declarations interact with independently maintained stubs.

The draft also requires a checker targeting a different Python version to find packages for the target version. Running the checker under one interpreter does not establish which dependency installation should supply the types. That requirement connects type discovery to the environment whose program is being checked. The proposal consequently covers both the distribution of an interface description and the selection of the environment in which that description is meant to apply.