A stub file is a file containing a skeleton of the public interface of a Python module, including classes, variables, functions, and most importantly, their types.
What are stubs?
Stub files use the .pyi extension and contain type information without implementation:
Implementation (csv_utils.py)
Stub (csv_utils.pyi)
def read_csv ( filename , encoding = 'utf-8' ):
"""Read a CSV file and return rows."""
with open (filename, encoding = encoding) as f:
# ... implementation ...
return rows
Mypy uses the stub file for type checking, while Python uses the implementation at runtime.
Why use stubs?
Third-party libraries Add types to libraries you don’t control
Legacy code Add types without modifying source
C extensions Provide types for C/C++ extension modules
Performance Separate type information from runtime code
Creating stub files
Manual creation
Create .pyi file
Create a stub file with the same name as the module: # For module mymodule.py
touch mymodule.pyi
Add type signatures
Write the public interface with types: # mymodule.pyi
class MyClass :
attribute: int
def method ( self , x : str ) -> bool : ...
def helper ( x : int ) -> str : ...
Configure MYPYPATH
If stubs are in a separate directory: export MYPYPATH =~ / myproject / stubs
Using stubgen
Mypy includes stubgen to automatically generate stubs:
# Generate stub for a module
stubgen mymodule.py
# Generate stub for a package
stubgen -p mypackage
# Generate stubs for installed package
stubgen -p requests
See stubgen command reference for details.
Stub file syntax
Stub files use normal Python syntax with some conventions:
Variables
# Variables with annotations don't need values
default_encoding: str
MAX_SIZE : int
# Use Literal for constants
from typing import Literal
DEBUG : Literal[ True ]
Functions
# Use ellipsis for function bodies
def greet ( name : str ) -> None : ...
# Default arguments use ellipsis
def connect ( host : str , port : int = ... ) -> Connection: ...
# Overloads
from typing import overload
@overload
def process ( x : int ) -> int : ...
@overload
def process ( x : str ) -> str : ...
def process ( x : int | str ) -> int | str : ...
Classes
class Connection :
host: str
port: int
def __init__ ( self , host : str , port : int = ... ) -> None : ...
def send ( self , data : bytes ) -> None : ...
def close ( self ) -> None : ...
Type aliases
from typing import Union
JSON = Union[ dict , list , str , int , float , bool , None ]
def parse_json ( data : str ) -> JSON : ...
Stub file locations
Mypy searches for stubs in several locations:
Inline stubs
Place .pyi file next to .py file:
myproject/
├── mymodule.py
└── mymodule.pyi # Takes precedence
The .pyi file takes precedence over .py for type checking.
Stub directory
Use a dedicated stubs directory:
myproject/
├── src/
│ └── mypackage/
│ └── module.py
└── stubs/
└── mypackage/
└── module.pyi
Configure MYPYPATH:
export MYPYPATH =~ / myproject / stubs
Installed stub packages
Install PEP 561 stub packages:
pip install types-requests
pip install types-redis
Mypy finds these automatically.
Stub-only packages must be installed - they cannot be used via MYPYPATH.
PEP 561 stub packages
Using stub packages
Many popular libraries have stub packages:
# Install stubs for requests
pip install types-requests
# Install stubs for multiple packages
pip install types-redis types-PyYAML types-toml
# Auto-install missing stubs
mypy --install-types
Creating stub packages
To distribute stubs for a library:
Create package structure
mylibrary-stubs/
├── setup.py
└── mylibrary-stubs/
├── __init__.pyi
└── submodule.pyi
Configure setup.py
from setuptools import setup
setup(
name = "mylibrary-stubs" ,
version = "1.0.0" ,
packages = [ "mylibrary-stubs" ],
package_data = { "mylibrary-stubs" : [ "*.pyi" ]},
)
Add py.typed marker
touch mylibrary-stubs/py.typed
Typeshed
Typeshed is a repository of stubs for the Python standard library and popular third-party packages.
Mypy includes a copy of typeshed for:
Python standard library
Common third-party libraries
Contributing to typeshed
To add stubs for a library:
Fork the typeshed repository
Add stubs in stubs/libraryname/
Add a METADATA.toml file
Submit a pull request
See typeshed contributing guide .
Partial stubs
You don’t need to stub everything:
# mymodule.pyi
# Stub only the functions you use
def important_function ( x : int ) -> str : ...
# Leave out internal implementation details
# _internal_helper not included
Mypy will infer types from implementation for unst ubbed members.
Stub testing with stubtest
Verify stubs match implementation:
This catches:
Missing functions or classes
Incorrect signatures
Type mismatches
See stubtest command reference for details.
Common patterns
import sys
if sys.platform == "win32" :
def windows_only () -> None : ...
else :
def unix_only () -> None : ...
Version-specific code
import sys
if sys.version_info >= ( 3 , 10 ):
def new_function () -> None : ...
Re-exports
# Make imported names visible
from submodule import helper as helper
from submodule import MyClass as MyClass
# Or use __all__
__all__ = [ "helper" , "MyClass" ]
Use Any when you don’t know the type:
from typing import Any
def complex_function ( x : Any) -> Any: ...
Best practices
Start with stubgen Generate initial stubs automatically, then refine manually
Use stubtest Regularly verify stubs match implementation
Contribute upstream Share useful stubs with typeshed or the library maintainers
Keep stubs simple Stubs should contain only type information, no implementation
Troubleshooting
Stub not being used
Check stub discovery:
mypy --verbose myproject | grep ".pyi"
Conflicting stubs
If multiple stubs exist, mypy uses this priority:
Inline stubs (.pyi next to .py)
Installed PEP 561 packages
Stubs in MYPYPATH
Typeshed
Incomplete stubs
Allow partial stubs:
# mypy.ini
[mypy]
allow_incomplete_stubs = True
Don’t point MYPYPATH to site-packages or the standard library - this will cause errors.
Examples
Stubbing a simple module
# calculator.py
class Calculator :
def __init__ ( self ):
self .memory = 0
def add ( self , x , y ):
return x + y
def store ( self , value ):
self .memory = value
Stubbing with overloads
# formatters.pyi
from typing import overload
@overload
def format_value ( value : int ) -> str : ...
@overload
def format_value ( value : float , precision : int = ... ) -> str : ...
@overload
def format_value ( value : str ) -> str : ...
def format_value ( value : int | float | str , precision : int = ... ) -> str : ...
Use stubs to add types to third-party libraries without waiting for upstream support!