Type, type, type.

Python will be much easier if there are no type hints.

But Python will be less robust if there are really no type hints.

After all, type safety prevents the chaos of hidden bugs.

Behind the scenes, Python's typing system is more than just inline type hints. There are two significant concepts you may overlook: type stubs and typeshed.

If you've ever wondered what .pyi files are, how they relate to your code, and when you should use them, this article is for you.

What Are Type Stubs?

Type stubs are files with the .pyi extension.

They look like Python files, but instead of containing real code, they only contain function signatures, classes, and type hints.

We can think of them as "blueprints" of a module: they describe what exists and its types, but not how it works.

For example, if we have a simple function like the following:

# math_utils.py
def add(a, b):
    return a + b

This file has no type hints. We can create a stub file for it like this:

# math_utils.pyi (type stub)
def add(a: int, b: int) -> int: ...

Now, a type checker knows that add() takes two integers and returns an integer, even though the real function in math_utils.py has no type annotations at all.

Of course, we mostly don't need to write a separate file just for basic type hints, especially when the inline hints are not complicated.

But the mechanism of type stubs will show its necessity when the program and its typing become complex.

Because the philosophy of the type stubs is: separation of concerns (SoC). It is a design principle that says:

Each part of a program should deal with one, and only one, responsibility.

Have you ever struggled with a large Python codebase where the logic and type hints are all mixed up together?

If you are, you need to understand type stubs.

What Is Typeshed?

There are so many Python modules that were made before the type hint syntax matured. Does the Python community have to rewrite all the source code?

No need. Thanks to the mechanism of type stubs. There is an official and huge repository covering all the fundamental type stubs we need — typeshed. It covers:

  • The Python standard library (str, os, json, etc.)
  • Many popular third-party packages (requests, dateutil, etc.)

Under the hood, modern type checkers and IDEs rely on typeshed so that they can analyze our code without us annotating everything ourselves.

When you use os.path.join, for instance, a modern IDE, such as PyCharm, can suggest completions and catch type errors, even though CPython's implementation doesn't include those annotations.

Simply put, typeshed is essentially the "official database" of type stubs for Python.

Fortunately, as long as you have installed the modern type checker, such as mypy, pyright, or you are using an IDE like PyCharm, you don't need to install typeshed separately. These tools ship with a copy of typeshed built in.

When Should You Use Type Stubs?

In most cases, the inline type hints are handy enough for us, and type checkers already use typeshed under the hood, so we don't need to worry too much about type stubs.

However, in some scenarios, writing separate type stubs could be a better choice:

  • If you would like to quickly develop a module without considering types, a good idea is to implement the logic first, and then ship .pyi stubs when it's necessary.
  • If you are maintaining an old code base, and don't want to take the risks of touching the core logic. Writing external type stubs will make more sense.
  • If you are using third-party libraries without inline type hints and external stubs, to avoid hidden bugs that can't be found by type checkers, you may need to write .pyi files by yourself.

Type checkers, by the way, give precedence to stub files when stubs and inline annotations coexist.

Practical Demo: From Untyped Code to Stubs

Talk is cheap. Let's walk through a real but simple example to see how to generate a type stub for a Python file quickly.

Step 1: Start with untyped code

Here is a simple example code file with no type hints.

# greeter.py
def greet(name):
    return "Hello " + name
def repeat(msg, times):
    return [msg] * times

Step 2: Generate a stub file with mypy

We will use the popular type-checking tool, mypy, to generate a .pyi file. The usage of other tools is similar.

First of all, let's install the mypy module:

pip install mypy

Then we can use the built-in stubgen method of mypy to generate the stub file of the greeter.py:

stubgen greeter.py

This creates a file as follows:

# greeter.pyi (auto-generated)
def greet(name): ...
def repeat(msg, times): ...

The tool doesn't know the real types yet, so everything is just a template for now. But it saves us time from rewriting the function signatures.

Step 3: Refine the stub manually

Now, we can edit the greeter.pyi stub to add real type hints:

# greeter.pyi (refined)
def greet(name: str) -> str: ...
def repeat(msg: str, times: int) -> list[str]: ...

Step 4: Run a type checker

Everything is set up now. Let's write a quick test to check if it works:

# test.py
import greeter

numbers = greeter.repeat(4, 5)
print(numbers)
# [4, 4, 4, 4, 4]

The above code can be executed smoothly and print the result as expected. But don't say it's correct before doing type checking with mypy:

mypy test.py

The output is:

test.py:3: error: Argument 1 to "repeat" has incompatible type "int"; expected "str"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

Bingo! Mypy caught the error perfectly thanks to the stub.

Key Takeaways

The concept of type stubs is important for a type-safe Python ecosystem. Even if we don't have to handle them frequently, we must understand that:

  • Type stubs (.pyi files) describe code structure and types, not behavior.
  • Typeshed is the central repository of type stubs for Python's standard library and many third-party libraries.
  • Modern type checkers and IDEs are bundled with typeshed, so we don't need to install it independently in most cases.
  • Type checkers always prefer stubs over inline hints.
  • Inline hints in .py files are enough for our daily coding. But the .pyi stubs are necessary when we need external type definitions to keep runtime code clean and neat.

Next time your IDE autocompletes a standard library function or warns you about a type mismatch, remember, it's type stubs and typeshed doing the heavy lifting behind the scenes.

Thanks for reading! If you are interested, here are more articles about Python's typing system: