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 stacks, databases) 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

Include every parameter with its type annotation and the return type. That’s the whole point of the file. Use ... for the body, which is the standard Python way to mark a stub. Import only what the type hints need (pandas here, for pd.DataFrame). The code can’t run, but Pyright can still 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:

  1. Looks in site-packages/phase_gap/__init__.py
  2. Reads the stub signature
  3. Validates that netcdf_file="foo.nc" matches netcdf_file: str
  4. Infers tidal_df has type pd.DataFrame
  5. Never tries to execute the ... implementation

Benefits

Writing a text file is faster than installing packages, and it skips the heavy dependencies (C libraries, databases) entirely. Pyright still catches type errors. The ... makes it obvious to a reader that the module exists only for type checking, and the script that generates it can go into version control.

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.

See Also