Creating Type Stubs for Type Checking Without Installing Dependencies
The Problem
When type-checking Python code, tools like Pyright need to import all dependencies to understand function signatures. But some packages have heavy dependencies (NetCDF libraries, scientific computing tools, databases, etc.) that you don’t want to install just for type checking.
The Solution: Manual Type Stubs
You can create a fake package in your virtual environment that provides type information without any implementation.
How It Works
Python’s import system doesn’t distinguish between packages installed via pip
and packages manually created in site-packages. Both are treated identically.
# These are equivalent to Python's import system:
pip install phase-gap # Real package
mkdir -p .venv/lib/python3.X/site-packages/phase_gap # Fake stub
Example Implementation
#!/bin/bash
# Create path to stub package
STUB_DIR=".venv/lib/python$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')/site-packages/phase_gap"
mkdir -p "$STUB_DIR"
# Create minimal __init__.py with type signatures only
cat > "$STUB_DIR/__init__.py" << 'EOF'
"""Type stubs for phase-gap package (type checking only)."""
import pandas as pd
def run_compute_job(
netcdf_file: str,
export_csv: bool = False,
output_filename: str | None = None,
verbose: bool = False
) -> pd.DataFrame:
"""Process NetCDF tidal data."""
... # Ellipsis = stub implementation
def run_analysis(
external_tidal_data: pd.DataFrame,
external_profile_data: pd.DataFrame,
output_path: str
) -> int:
"""Run analysis. Returns error code (0 = success)."""
...
EOF
Key Points
- Function signatures with types - Include all parameters with type annotations and return types
- Ellipsis (
...) for implementation - Standard Python way to indicate stub/placeholder - Minimal imports - Only import what’s needed for type hints (e.g.,
pandasforpd.DataFrame) - No actual logic - The code can’t run, but Pyright can validate against it
What Pyright Sees
When analysing your code:
from phase_gap import run_compute_job
tidal_df = run_compute_job(netcdf_file="foo.nc")
Pyright:
- Looks in
site-packages/phase_gap/__init__.py - Reads the stub signature
- Validates that
netcdf_file="foo.nc"matchesnetcdf_file: str - Infers
tidal_dfhas typepd.DataFrame - Never tries to execute the
...implementation
Benefits
- Fast setup - Create a text file instead of installing packages
- No bloat - Avoid heavy dependencies (C libraries, databases, etc.)
- Type safety - Pyright still catches type errors
- Clear intent - The
...makes it obvious this is type-checking only - Version control friendly - Can check in the stub creation script
Alternative: .pyi Files
The official Python approach is .pyi stub files:
# phase_gap.pyi
import pandas as pd
def run_compute_job(
netcdf_file: str,
export_csv: bool = ...,
output_filename: str | None = ...,
verbose: bool = ...
) -> pd.DataFrame: ...
Both approaches work, but creating a minimal Python module is simpler and
doesn’t require understanding the .pyi stub file format.
Real-World Use Case
In our project, phase-gap has dependencies on:
- NetCDF4 (C library bindings)
- XBeach simulation tools
- Matplotlib, NumPy, Pandas
- Various scientific computing libraries
For type checking analysis.py, we only need to know:
- What functions exist
- What parameters they accept
- What they return
Creating a stub lets us type-check without installing any of the heavy dependencies!