Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 11 – Type hints and static type checking

Typing libraries: py.typed and stub files

Ship type information with a package using py.typed, write .pyi stub files, see where checkers find types, and check code that uses untyped libraries.

  • Advanced
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: Inline types count only with a py.typed marker · A .pyi stub file describes a module for the checker · A checker looks for types in a fixed order · Check yourself. With a pass: 2 in-depth parts and 3 interview answers, about 12 minutes.

What you will learn

  • Package a library's inline types for checkers with a py.typed marker
  • Write .pyi stub files for a module that has no annotations
  • Explain where a checker looks for the types of an import, from your own stubs to installed packages
  • Generate a draft stub with stubgen and compare it with the module using stubtest
  • Check code that imports untyped libraries without hiding errors

Before you start

What you’ll learn · Before you start

What you will learn

  • Package a library's inline types for checkers with a py.typed marker
  • Write .pyi stub files for a module that has no annotations
  • Explain where a checker looks for the types of an import, from your own stubs to installed packages
  • Generate a draft stub with stubgen and compare it with the module using stubtest
  • Check code that imports untyped libraries without hiding errors

Before you start

On this page

Two installed packages define the same annotated function, and the same mistake is made with each: in this lesson’s run, mypy 2.4.0 reported it for one package and said nothing for the other, because only the reported package contains an empty marker file called py.typed. A type checker can only check a call into a library if it knows the library’s types.

It learns them in one of three ways. A package can carry inline annotations and say so with the py.typed marker: An empty file named py.typed in a package's folder (PEP 561). It tells type checkers that the package's inline annotations or .pyi files are meant for them; without it, mypy treats an installed package as untyped. (PEP 561). A stub file: A file with the suffix .pyi that holds only a module's interface: signatures with ... as their bodies, and annotated variables. Type checkers read it in place of the .py module beside it; Python never imports it., .pyi, holds only signatures, written in Python syntax with ... for every body, and checkers read it instead of the .py module. And The shared collection of stub files for the standard library and many third-party packages. Checkers bundle its standard-library part, and its third-party stubs are published as types-… packages., the shared collection of stubs for the standard library and many third-party packages, fills in the rest: checkers bundle its standard-library part, and its third-party stubs are published as types-… packages. When none of these has a module, the checker reports the import and treats everything from it as Any, so calls into it are not checked at all.

Inline types count only with a py.typed marker

Two installed packages, shopkit and legacy, have the same annotated function. Only shopkit has the marker:

site/shopkit/__init__.py, beside an empty py.typed Python · installed/site/shopkit/__init__.py
"""An installed package with inline annotations and a py.typed marker."""


def price_with_tax(price: int, rate: float = 0.18) -> int:
    return round(price * (1 + rate))
site/legacy/__init__.py, without py.typed Python · installed/site/legacy/__init__.py
"""An installed package with the same annotations, but without a py.typed marker."""


def price_with_tax(price: int, rate: float = 0.18) -> int:
    return round(price * (1 + rate))
checkout.py uses both Python · installed/checkout.py
from legacy import price_with_tax as legacy_price
from shopkit import price_with_tax

label: str = price_with_tax(100)
other: str = legacy_price(100)
mypy on checkout.py Python · installed/check_checkout.py
"""Runs `mypy --strict checkout.py` with site/ as a folder of installed packages, as site-packages is after pip install."""
import sys

from mypy import api

sys.path.append("site")
print("$ mypy --version")
print(api.run(["--version"])[0], end="")
print("$ mypy --strict checkout.py")
report, errors, status = api.run(["--strict", "checkout.py"])
print(report + errors, end="")

Output

$ mypy --version
mypy 2.4.0 (compiled: yes)
$ mypy --strict checkout.py
checkout.py:1: error: Skipping analyzing "legacy": module is installed, but missing library stubs or py.typed marker  [import-untyped]
checkout.py:1: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
checkout.py:4: error: Incompatible types in assignment (expression has type "int", variable has type "str")  [assignment]
Found 2 errors in 1 file (checked 1 source file)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_checkout.py

Like the other check files here, it calls mypy 2.4.0 from Python; with that version installed, the command after the dollar sign does the same job in a terminal. The check script puts the folder site/ on the path where Python finds installed packages, the role that site-packages plays after pip install, so mypy treats both packages as installed. It used the inline types of shopkit and reported the int assigned to a str. It skipped legacy with “missing library stubs or py.typed marker”, so the same mistake two lines later went unreported. Step through the file as mypy checked it:

Interactive · stage by stage One file, two packages, one marker Step through checkout.py line by line, as mypy checks it. Both packages define the same annotated function.
One file, two packages, one marker, step 6 of 6. Showing from legacy import ..., stubs or py.typed?, from shopkit import ..., py.typed found, label: str = ... (line 4: reported), other: str = ... (line 5: not reported, Any). In focus: other: str = ....from legacy import ...line 1stubs or py.typed?neither: the package is skippedfrom shopkit import ...line 2py.typed foundinline hints are usedlabel: str = ...line 4: reportedother: str = ...line 5: not reported, Any

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The first import. Line 1 imports price_with_tax from legacy, an installed package whose function is fully annotated.
  2. No marker, no types. mypy finds no stub for legacy and no py.typed file in its folder, so it skips the package and reports the import as untyped, with "missing library stubs or py.typed marker".
  3. The second import. Line 2 imports the same function from shopkit, whose folder holds an empty py.typed file beside __init__.py.
  4. The marker opts in. The marker says that the package's annotations are meant for checkers, so mypy reads its inline hints and learns that price_with_tax() returns an int.
  5. A mistake reported. Line 4 stores that int in a variable annotated str, and mypy reported it as an incompatible assignment.
  6. The same mistake, missed. Line 5 makes the same mistake with legacy's function. Because legacy was skipped, the function is Any for mypy, and nothing was reported.

The annotations in legacy are fine; the package simply never said that they are meant for checkers. That is why a library with inline hints needs the py.typed file in its package folder, and why the file must reach the wheel that users install.

Checkpoint

Check: The py.typed marker

  1. Question 1 of 2 What did mypy do with from legacy import price_with_tax in checkout.py?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It skipped the package, reported the import as untyped, and treated the function as Any

    For an installed package, mypy uses inline hints only when the package opts in with py.typed. legacy did not, so its function was Any and the same mistake two lines later passed.

  2. Question 2 of 2 A library's repository has py.typed in its package folder, yet in users' projects mypy reports "missing library stubs or py.typed marker" for it. What should its maintainers check first?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: That the wheel users install actually contains py.typed

    The file must reach the wheel that users install. When the message appears although the repository has the marker, the build is the first suspect.

A .pyi stub file describes a module for the checker

geo.py was written without annotations, and nobody wants to change it. A stub beside it, geo.pyi, describes it:

geo.pyi: the interface of geo.py, without bodies PYI · stubs/geo.pyi
from collections.abc import Mapping

type _LatLon = tuple[float, float]

EARTH_RADIUS_KM: float

def distance_km(a: _LatLon, b: _LatLon) -> float: ...
def nearest(origin: _LatLon, places: Mapping[str, _LatLon], limit: int = 1) -> list[str]: ...
trip.py imports the untyped module Python · stubs/trip.py
from geo import distance_km, nearest

PUNE = (18.52, 73.86)
MUMBAI = (19.08, 72.88)
places = {"Mumbai": MUMBAI, "Goa": (15.50, 73.83), "Nashik": (20.00, 73.79)}

print(f"{distance_km(PUNE, MUMBAI):.0f} km to Mumbai")
print("nearest:", nearest(PUNE, places, limit=2))
try:
    nearest(PUNE, ["Mumbai", "Goa"])
except TypeError as error:
    print("TypeError:", error)
km: int = distance_km(PUNE, MUMBAI)
print("km holds a", type(km).__name__)

Output

120 km to Mumbai
nearest: ['Mumbai', 'Nashik']
TypeError: list indices must be integers or slices, not str
km holds a float

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 trip.py

mypy on trip.py Python · stubs/check_trip.py
"""Runs `mypy --strict trip.py` through mypy's Python API and prints what the command prints."""
from mypy import api

print("$ mypy --version")
print(api.run(["--version"])[0], end="")
print("$ mypy --strict trip.py")
report, errors, status = api.run(["--strict", "trip.py"])
print(report + errors, end="")

Output

$ mypy --version
mypy 2.4.0 (compiled: yes)
$ mypy --strict trip.py
trip.py:10: error: Argument 2 to "nearest" has incompatible type "list[str]"; expected "Mapping[str, tuple[float, float]]"  [arg-type]
trip.py:13: error: Incompatible types in assignment (expression has type "float", variable has type "int")  [assignment]
Found 2 errors in 1 file (checked 1 source file)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_trip.py

geo.py, the module itself Python · stubs/geo.py
"""Distances between places: a helper module written without annotations."""
import math

EARTH_RADIUS_KM = 6371.0


def distance_km(a, b):
    lat1, lon1 = map(math.radians, a)
    lat2, lon2 = map(math.radians, b)
    h = math.sin((lat2 - lat1) / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin((lon2 - lon1) / 2) ** 2
    return 2 * EARTH_RADIUS_KM * math.asin(math.sqrt(h))


def nearest(origin, places, limit=1):
    return sorted(places, key=lambda name: distance_km(origin, places[name]))[:limit]

A stub next to a module takes its place for the checker, so mypy read geo.pyi and reported both mistakes in trip.py: a list passed where a mapping of places is expected, and a float stored in a variable annotated int. Python never reads the stub. It ran the real module, which failed on the list and returned a float, exactly as the two reports predicted. Step through the import with each of its two readers:

Interactive · step by step One import, two readers Step through what each reader does with the import on line 1 of trip.py. The left column is mypy, the right column is Python.
One import, two readers, step 6 of 6. Showing trip.py, geo.pyi (type _LatLon = tuple[...]), mypy: 2 errors, geo.py, the run. In focus: geo.pyi.mypy readsPython importstrip.pyfrom geo import ...geo.pyitype _LatLon = tuple[...]mypy: 2 errorslines 10 and 13geo.pyno annotations at allthe runTypeError, and a float

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The import. trip.py imports distance_km() and nearest() from geo, a module written without annotations. Two of its lines are wrong.
  2. mypy finds the stub. mypy looks for geo, finds geo.pyi beside geo.py, and reads the stub in place of the module.
  3. Two mistakes reported. With the stub's types, mypy reported line 10, a list passed where a mapping of places is expected, and line 13, a float stored in a variable annotated int.
  4. Python runs the module. Python never reads the stub. import geo runs geo.py, the module without annotations, as if the stub were not there.
  5. The run agrees. The real nearest() failed on the list with TypeError, and km held a float, exactly the two problems mypy had reported before the run.
  6. Stub-only names. The alias _LatLon exists only in geo.pyi, and its underscore keeps it out of geo's public names. The checker uses it to describe the coordinates; Python never sees it.

The stub’s alias, type _LatLon = ..., exists only in the stub. Its underscore follows the stub-writing guide, which asks for one on names a stub adds only to be readable: geo.py has no such name, so nobody should import it from geo. The ... bodies are all a stub file ever contains. The standard library is described the same way: when an editor shows the parameters of json.loads(), it reads a stub from typeshed that ships with the checker, not the module’s source.

Key idea

The checker reads the stub and Python runs the module, so a stub can give types to code you cannot change, and a wrong stub misleads only the checker.

Checkpoint

Check: What a .pyi stub file is

  1. Question 1 of 2 geo.py has no annotations, and geo.pyi sits beside it. Which file does Python run when trip.py imports geo?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: geo.py, because Python never reads stub files

    The checker reads geo.pyi in place of geo.py, and Python imports geo.py as if the stub were not there. A stub changes what the checker knows, never what runs.

  2. Question 2 of 2 The stub says nearest() takes a mapping of places, and trip.py passes a list. What happens when the program runs that call?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The real nearest() fails with TypeError, the problem mypy reported on line 10

    The run failed where the checker had said it would. A good stub lets the checker predict the run's errors before the program runs.

A checker looks for types in a fixed order

Six places a checker tries in order for the types of an import, from your own stubs to installed packages.import shopkit1. your search path firstMYPYPATH or mypy_path: your own stubs and patches2. the code being checkedyour project's own files3. typeshed for the standard librarybundled with the checker4. a stub-only packageshopkit-stubs, found before the package itself5. the installed packageits inline hints or .pyi files, only with py.typed6. stubs the checker ships for third-party packageslast, if anynothing foundthe import is reported, and the module is Any

Where a type checker looks for the types of an import

Text description of the diagram

The diagram is a list of steps from top to bottom, starting with the statement import shopkit. The checker takes the types from the first place that has them.

  1. Your own search path first: the folders in MYPYPATH or the mypy_path option, where you can put your own stubs or patch broken ones.
  2. The code being checked: your project's own files.
  3. Typeshed's stubs for the standard library, which the checker bundles.
  4. A stub-only package named shopkit-stubs, which is used before the package itself.
  5. The installed package: its inline hints or the .pyi files inside it, but only when it contains a py.typed marker.
  6. Stubs that the checker itself ships for third-party packages, if it ships any; these come last.

If none of them has the module, the checker reports the import, and the module and everything from it is treated as Any.

The typing specification fixes this order, and mypy’s documentation lists the same places for its own search path. Two consequences matter in practice. Your own search path comes first, so a stub you write for a broken or untyped library wins over everything else, without editing the library. And a stub package: A package that adds type hints for a library that ships without them, such as types-PyYAML for PyYAML. It is installed beside the type checkers, as a development dependency. More in “mypy and pyright: running type checkers”, named after the runtime package with -stubs added, wins over the package’s own inline types, which is how typeshed’s types-… packages add types to libraries that ship none. mypy also knows which libraries have such packages: for those, its report says “Library stubs not installed” and names the package to install, such as types-PyYAML for yaml, instead of the general message about a missing marker. Follow four imports of this lesson through the order:

Interactive · stage by stage Four imports through the search order Step through the order in which a checker looks for the types of an import. Each step follows one import of this lesson until a place has its types.
Four imports through the search order, step 6 of 6. Showing 1. your search path, 2. the code being checked (geo: geo.pyi found), 3. typeshed: stdlib (json: found in typeshed), 4. a stub-only package, 5. the installed package (shopkit: py.typed, used), 6. bundled third-party, nothing found (legacy: reported, then Any). In focus: 4. a stub-only package.1. your search pathMYPYPATH or mypy_path2. the code being checkedgeo: geo.pyi found3. typeshed: stdlibjson: found in typeshed4. a stub-only packagenamed with -stubs5. the installed packageshopkit: py.typed, used6. bundled third-partystubs the checker shipsnothing foundlegacy: reported, then Any

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. import geo. trip.py imports geo. Nothing on the search path has it, but the code being checked does, geo.pyi beside trip.py, so the search stops at the second place.
  2. import json. json is part of the standard library. The first two places have no json, and the copy of typeshed that the checker bundles does, at the third place.
  3. import shopkit. shopkit is an installed package with no stubs anywhere and no stub-only package, so the search reaches the package itself, whose py.typed marker makes its inline hints count.
  4. import legacy. legacy is installed without py.typed, and no place has stubs for it. The search runs out, mypy reports the import, and everything from legacy is Any.
  5. Your own stubs win. A stub for any of these packages in a folder on MYPYPATH would be found at the first place, before everything else, which is how you correct a library's types without editing it.
  6. Stub-only packages. A package called shopkit-stubs would win at the fourth place over shopkit's own hints. typeshed's types-… packages add types to libraries that ship none in that place.

The four sources a library’s users meet, side by side:

Source of types Who writes it What the checker needs
Inline annotations in a package The library’s authors A py.typed marker in the package
.pyi files inside a package The library’s authors A py.typed marker as well
A stub-only package, name-stubs Anyone, often typeshed’s contributors Nothing more: it is used when installed
Your own stubs on the search path You A folder listed in MYPYPATH or mypy_path

Checkpoint

Check: Where a checker looks for types

  1. Question 1 of 2 You put a stub for shopkit in a folder on MYPYPATH, and shopkit itself ships py.typed with inline hints. Which types does mypy use?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Your stub on MYPYPATH, because the search path comes first

    The search order starts with your own search path, then your code, typeshed's standard library, stub-only packages and the installed package, so your stub overrides the package's own hints.

  2. Question 2 of 2 A stub-only package shopkit-stubs is installed beside shopkit, which has py.typed and inline hints. Which types does the checker use for import shopkit?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: shopkit-stubs, because stub-only packages come before the package itself

    A stub-only package wins over the package's own inline types, which is how typeshed's types-… packages give types to libraries without changing them.

Check yourself

One question at a time, beginning with the source of types you know least.

Practice that adapts to you

Practise this lesson

9 questions about 11 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 9 An installed package has complete inline annotations but no py.typed file. What does mypy do with an import of it?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: It reports the import as an untyped module and treats the package as Any

    The py.typed marker is how a package says that its inline types are meant for checkers. In the recorded run, mypy skipped legacy, which has the same annotations as shopkit but no marker, and so it could not report the wrong assignment from it.

  2. Question 2 of 9 A package's folder holds an __init__.py with complete annotations. What is the smallest change that makes mypy use them in users' projects?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: Add an empty py.typed file beside __init__.py, and ship it in the wheel

    One empty file, in the right folder and in the built wheel, turns the package's existing annotations into checks in every user's project.

  3. Question 3 of 9 mypy reports "Library stubs not installed" for yaml and names types-PyYAML, but reports "missing library stubs or py.typed marker" for your company's internal package. What does the difference tell you?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: typeshed has stubs for yaml, while your package has neither stubs nor a marker

    The message names a stub package only for libraries that have one. The general message means you must add the marker to your own package, or write stubs for it.

  4. Question 4 of 9 Which of these belong in a .pyi stub file?

    Choose every answer that is right.

    Show the answer: Question 4 of 9

    Answer:

    • Annotated module variables such as EARTH_RADIUS_KM: float
    • Function signatures with ... as their body

    A stub holds the interface only: signatures, annotated variables and classes with annotated methods, every body written as .... Python never runs a stub; checkers read it instead of the .py file.

  5. Question 5 of 9 geo.py and geo.pyi sit in the same folder. Which one does mypy read when it checks trip.py, which imports geo?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: geo.pyi

    A stub next to a module takes its place for the checker. That is how mypy knew the types of the untyped geo.py and reported the list passed to nearest().

  6. Question 6 of 9 What does trip.py print?

    What does this program print? Choose one answer.

    from geo import distance_km, nearest
    
    PUNE = (18.52, 73.86)
    MUMBAI = (19.08, 72.88)
    places = {"Mumbai": MUMBAI, "Goa": (15.50, 73.83), "Nashik": (20.00, 73.79)}
    
    print(f"{distance_km(PUNE, MUMBAI):.0f} km to Mumbai")
    print("nearest:", nearest(PUNE, places, limit=2))
    try:
        nearest(PUNE, ["Mumbai", "Goa"])
    except TypeError as error:
        print("TypeError:", error)
    km: int = distance_km(PUNE, MUMBAI)
    print("km holds a", type(km).__name__)
    Show the answer: Question 6 of 9

    Answer: it prints

    120 km to Mumbai
    nearest: ['Mumbai', 'Nashik']
    TypeError: list indices must be integers or slices, not str
    km holds a float

    Python ignores the stub: the list really is passed to nearest(), which fails when it indexes the list with a name, and km holds whatever distance_km() returned, a float, whatever its annotation says. mypy reported both lines before the run.

  7. Question 7 of 9 A compiled extension module has no Python source a checker could read. How does a checker get its types?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: From a .pyi stub that describes its interface

    A stub describes any module from outside, whether it is untyped Python, a library you cannot change, or code with no Python source at all.

  8. Question 8 of 9 Put these places in the order in which a checker looks for the types of an installed package called shopkit.

    Give each item its position, from 1 (first).

    Show the answer: Question 8 of 9

    Answer:

    1. Stubs on your own search path, such as MYPYPATH
    2. A stub-only package called shopkit-stubs
    3. The installed shopkit package, if it has py.typed
    4. Third-party stubs that the checker ships

    The typing specification gives the order: your own search path first, then your code and the standard library's stubs, then a stub-only package, then the package's own types when it opts in with py.typed, and the checker's bundled third-party stubs last.

  9. Question 9 of 9 One function of shopkit has a wrong return annotation, and you cannot change shopkit. Where do you correct it so that your checker uses the fix?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: In your own stub for shopkit, in a folder on MYPYPATH

    Your own search path is the first place in the order, which makes it the place to correct a library's types without editing the library.

Writing stubs that checkers trust: stubgen, stubtest and a stub that lies, reproduced Pro: opens with a Pro, Premium or Ultimate pass

A stub is a second description of a module, and two descriptions can disagree. This part drafts a stub with stubgen, compares a stub with its module using stubtest, and reproduces the kind of disagreement that neither tool can see.

In this part:

  • Drafting a stub with stubgen
  • Stub syntax rules worth knowing
  • Comparing a stub with its module using stubtest
  • What stubtest cannot see: a wrong return type, reproduced
  • Exercise: a stub for an untyped module
  • Write the stub file for an untyped module
    • Results of the sample tests

1 diagram · 9 runnable examples (PYI, Python) · 1 exercise with sample tests · about 11 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Shipping types and living with untyped code: packaging, stub-only packages and the options compared Pro: opens with a Pro, Premium or Ultimate pass

Types help only when they reach the people who install the package, and most projects depend on at least one library without them. This part ships py.typed in a build, finds a stub-only package, compares the ways to handle an untyped dependency, and modernises old hints.

In this part:

  • Shipping py.typed and stubs in a build
  • A stub-only package, found before the package itself
  • An untyped dependency: the options compared, for your case
  • Modernising superseded hints

7 runnable examples (IN, PYI, Python, TOML) · about 11 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    What is a .pyi stub file, and when does a type checker read it instead of the .py module?

    A stub file has the same name as a module with the suffix .pyi and holds only its interface: function signatures with ... as their body, annotated module variables and classes with annotated methods. Python never imports or runs it. A type checker that finds a stub reads it instead of the module beside it, so a stub can describe a module written without annotations, a C extension, or a library you cannot change. Checkers find stubs next to the module, in a folder on their own search path such as MYPYPATH, in a stub-only package named after the library with -stubs added, and in typeshed, whose stubs for the standard library are bundled with each checker.

  2. Mid to senior

    Your team's internal library has complete inline type hints, but in other teams' projects mypy reports it as an untyped module and checks nothing that comes from it. Why, and what do you change in the library and its build?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to staff

    A strict mypy run in CI fails because a third-party dependency has no types. Compare the options: ignoring the import, writing stubs in your repository, installing or publishing a stub package, and contributing types upstream. Which do you choose, and when?

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    A stub says a function returns int, but at run time it sometimes returns None, and a crash in production was traced to that. How do you find such disagreements between stubs and code, and keep stubs honest in CI?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A package’s inline types count for mypy only when the package contains a py.typed marker.
  • A .pyi stub holds signatures with ... bodies, and checkers read it instead of the module beside it.
  • Checkers look in a fixed order: your own stubs, your code, typeshed’s standard library, stub-only packages, then the package.
  • Without any of them, the import is reported and everything from it is Any.

Terms in this lesson

py.typed marker
An empty file named py.typed in a package's folder (PEP 561). It tells type checkers that the package's inline annotations or .pyi files are meant for them; without it, mypy treats an installed package as untyped.
stub file
A file with the suffix .pyi that holds only a module's interface: signatures with ... as their bodies, and annotated variables. Type checkers read it in place of the .py module beside it; Python never imports it.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.