Extending and embedding stubpy¶
This page shows how to go beyond the defaults: custom annotation rendering, docstring-embedded stubs, CI integration, and using the Python API directly.
Custom annotation handlers¶
Use register_annotation_handler() to teach stubpy
how to render annotation types it doesn’t recognise out of the box. This is
useful for Pydantic, attrs, beartype, and any other library that uses custom
annotation objects at runtime.
from stubpy.annotations import register_annotation_handler, annotation_to_str
# Suppose your library has an Annotated-like wrapper:
class Validated:
def __init__(self, inner):
self.inner = inner
@register_annotation_handler(lambda a: isinstance(a, Validated))
def _handle_validated(annotation, ctx):
inner_str = annotation_to_str(annotation.inner, ctx)
return f"Validated[{inner_str}]"
# Now stubs for functions annotated with Validated(int) emit Validated[int]
To run before built-in handlers, insert directly into the table:
from stubpy.annotations import _ANN_HANDLERS
_ANN_HANDLERS.insert(0, (my_predicate, my_handler))
Including docstrings in stubs¶
Use --include-docstrings (or include_docstrings = true in config) to
embed each symbol’s docstring as a triple-quoted body. This is useful when
the .pyi file serves as quick-reference documentation in your IDE:
stubpy mymodule.py --include-docstrings
# Without --include-docstrings (default)
def area(self, *, unit: str = "px") -> float: ...
# With --include-docstrings
def area(self, *, unit: str = "px") -> float:
"""Return the area in *unit* units."""
# stubpy.toml
include_docstrings = true
Using the Python API¶
stubpy’s Python API gives you full control:
from stubpy import generate_stub, generate_package, StubConfig, StubContext
# Fine-grained context per file
cfg = StubConfig(
include_private = True,
union_style = "legacy", # Optional[X] instead of X | None
include_docstrings = True,
)
ctx = StubContext(config=cfg)
stub_text = generate_stub("mymodule.py", ctx=ctx)
# Inspect diagnostics after the run
for d in ctx.diagnostics:
if d.level.name == "WARNING":
print(f" {d.symbol}: {d.message}")
# Package with custom context factory (one ctx per file)
result = generate_package(
"mypackage/",
output_dir="stubs/",
ctx_factory=lambda: StubContext(config=cfg),
)
print(result.summary())
for path, diags in result.failed:
print(f" FAILED: {path}")
CI / pre-commit integration¶
GitHub Actions:
- name: Generate stubs
run: |
pip install stubpy
stubpy src/ -o stubs/
git diff --exit-code stubs/ # fail if stubs are out of date
pre-commit hook (add to .pre-commit-config.yaml):
repos:
- repo: local
hooks:
- id: stubpy
name: Generate type stubs
entry: stubpy
args: [src/, -o, stubs/]
language: python
types: [python]
pass_filenames: false
Makefile target:
stubs:
stubpy src/ -o stubs/
check-stubs: stubs
git diff --exit-code stubs/
Execution modes¶
Three modes control whether the source module is actually imported:
# RUNTIME (default): import + full introspection
stubpy module.py
# AST_ONLY: parse only — safe for modules with side effects at import time
stubpy module.py --execution-mode ast_only
# AUTO: try runtime, fall back to AST-only on ImportError
stubpy module.py --execution-mode auto
from stubpy import generate_stub
from stubpy.context import ExecutionMode, StubConfig, StubContext
ctx = StubContext(config=StubConfig(execution_mode=ExecutionMode.AUTO))
stub = generate_stub("heavy_module.py", ctx=ctx)
# stubpy: ignore¶
Place # stubpy: ignore at the top of any .py file to skip it
entirely during stub generation (useful for generated code, C extensions,
or test helpers):
# stubpy: ignore
# This file is auto-generated — do not stub.
...
stubpy writes a minimal from __future__ import annotations stub and
records an INFO diagnostic.