Generate .pyi stub files with full **kwargs / *args MRO backtracing
Project description
stubpy
Generate .pyi stub files for Python modules with full **kwargs / *args MRO backtracing, type-alias preservation, generic support, overload stubs, and cross-file import resolution.
Features
**kwargsbacktracing — walks the entire MRO to expand**kwargsinto concrete, named parameters at every inheritance level.cls()detection —@classmethodmethods that forward**kwargsintocls(...)are resolved againstcls.__init__, not MRO siblings.- Typed
*argspreserved — explicitly annotated*args(e.g.*elements: Element) always survive the resolution chain. - Positional-only
/separator —def f(a, b, /, c)stubs correctly emit the PEP 570/separator. Parent positional-only params absorbed by**kwargsare promoted toPOSITIONAL_OR_KEYWORDto keep the child stub valid. - TypeVar / TypeAlias / NewType / ParamSpec / TypeVarTuple — declarations are re-emitted verbatim from the AST pre-pass, preserving bounds and constraints.
- Generic base classes —
class Stack(Generic[T]):is preserved correctly using__orig_bases__(PEP 560);__bases__erases the subscript. - @overload stubs — each
@overloadvariant gets its own stub; the concrete implementation is suppressed per PEP 484. - Type-alias preservation —
types.Lengthstaystypes.Lengthrather than expanding tostr | float | int. Works insideOptional[...],tuple[...],list[...], andUnion[..., None]too. - Cross-file imports — base classes and annotation types from other local modules are re-emitted in the
.pyiheader automatically. - AST pre-pass — a read-only AST harvest runs before (or instead of) module execution, recovering alias names that Python's
typing.Unionwould otherwise flatten. - Execution modes —
RUNTIME(default),AST_ONLY(no module execution), andAUTO(try runtime, fall back to AST-only on error). - Structured diagnostics — every pipeline stage records
INFO,WARNING, andERRORentries in aDiagnosticCollectorrather than swallowing exceptions silently. - Unified symbol table — classes, functions, variables, type aliases, and overload groups are all represented as typed
StubSymbolentries in aSymbolTable. - Dynamic typing imports —
from typing import ...header is built by scanningtyping.__all__with word-boundary matching, not a static list. - Zero runtime dependencies — stdlib only.
Environment setup
Requires Python 3.10+. The steps below use the Windows Python Launcher (
py).
On macOS / Linux replacepy -3.10withpython3.10.
# 1. Clone the repository
git clone https://github.com/wzjoriv/stubpy.git
cd stubpy
# 2. Create a virtual environment
py -3.11 -m venv .venv
# 3. Activate the environment
.venv\Scripts\activate # Windows CMD / PowerShell
# source .venv/bin/activate # macOS / Linux
# 4. Install in editable mode with development dependencies
pip install -e ".[dev]"
# 5. Verify — run the full test suite
pytest
How it works
stubpy runs an eight-stage pipeline, each stage in its own module:
generate_stub(filepath)
│
├─ 1. loader load_module() load source as a live module
│ └─ (skipped in AST_ONLY; warning+fallback in AUTO)
├─ 2. ast_pass ast_harvest() read-only AST pre-pass
├─ 3. imports scan_import_statements() parse AST → {name: import_stmt}
├─ 4. aliases build_alias_registry() discover type-alias sub-modules
├─ 5. symbols build_symbol_table() merge AST + runtime → SymbolTable
├─ 6. emitter for each symbol (source order):
│ ├─ AliasSymbol → generate_alias_stub()
│ ├─ ClassSymbol → generate_class_stub()
│ │ └─ for each method:
│ │ resolver resolve_params() ← MRO backtracing
│ │ emitter generate_method_stub() ← raw AST annotations
│ ├─ OverloadGroup → generate_overload_group_stub()
│ ├─ FunctionSymbol → generate_function_stub()
│ └─ VariableSymbol → generate_variable_stub()
├─ 7. imports collect_typing_imports() assemble header
│ collect_special_imports()
│ collect_cross_imports()
└─ 8. write .pyi file written to disk
resolve_params uses three strategies in order:
- No variadics — if the method has neither
*argsnor**kwargs, return its own parameters unchanged. cls()detection — if a@classmethodbody containscls(..., **kwargs), the**kwargsis resolved againstcls.__init__via AST analysis. Parameters hardcoded in the call are excluded.- MRO walk — walk ancestor classes that define the same method, collecting concrete parameters until all variadics are fully resolved. Parent
POSITIONAL_ONLYparams absorbed by**kwargsare promoted toPOSITIONAL_OR_KEYWORD.
StubContext carries all mutable state for one run. A fresh instance is created per generate_stub() call, making the generator fully re-entrant.
CLI
stubpy path/to/module.py # writes module.pyi alongside source
stubpy path/to/module.py -o out/module.pyi # custom output path
stubpy path/to/module.py --print # also print to stdout
stubpy path/to/module.py --include-private # include _private symbols
stubpy path/to/module.py --verbose # print all diagnostics to stderr
stubpy path/to/module.py --strict # exit 1 if any ERROR diagnostic
Python API
from stubpy import generate_stub
content = generate_stub("path/to/module.py")
content = generate_stub("path/to/module.py", "out/module.pyi")
Custom configuration
from stubpy import generate_stub
from stubpy import StubContext, StubConfig, ExecutionMode
# AST-only mode — no module execution
ctx = StubContext(config=StubConfig(execution_mode=ExecutionMode.AST_ONLY))
content = generate_stub("path/to/module.py", ctx=ctx)
# Strict mode — exit 1 on any ERROR diagnostic
ctx = StubContext(config=StubConfig(strict=True, verbose=True))
content = generate_stub("path/to/module.py", ctx=ctx)
if ctx.diagnostics.has_errors():
raise SystemExit(1)
Extended public API
# Diagnostics
from stubpy import DiagnosticCollector, DiagnosticLevel, DiagnosticStage, Diagnostic
# AST pre-pass
from stubpy import ast_harvest, ASTSymbols
# Symbol table
from stubpy import (
SymbolTable, SymbolKind,
ClassSymbol, FunctionSymbol, VariableSymbol, AliasSymbol, OverloadGroup,
build_symbol_table,
)
# Emitters (usable for custom stub generation)
from stubpy import (
generate_class_stub,
generate_function_stub,
generate_variable_stub,
generate_alias_stub,
generate_overload_group_stub,
)
Documentation (optional)
pip install -e ".[docs]"
cd docs && make html
# → open docs/_build/html/index.html in a browser
Installation (end users)
pip install stubpy
# or
uv add stubpy
Example
# shapes.py
from typing import TypeVar, Generic, overload
T = TypeVar("T")
class Shape:
def __init__(self, color: str = "black", opacity: float = 1.0) -> None: ...
class Circle(Shape):
def __init__(self, radius: float, **kwargs) -> None:
super().__init__(**kwargs)
@classmethod
def unit(cls, **kwargs) -> "Circle":
return cls(radius=1.0, **kwargs)
class Box(Generic[T]):
def put(self, item: T) -> None: ...
def get(self) -> T: ...
@overload
def parse(x: int) -> int: ...
@overload
def parse(x: str) -> str: ...
def parse(x):
return x
stubpy shapes.py --print
# shapes.pyi (generated)
from __future__ import annotations
from typing import Generic, TypeVar, overload
T = TypeVar('T')
class Shape:
def __init__(self, color: str = 'black', opacity: float = 1.0) -> None: ...
class Circle(Shape):
def __init__(
self,
radius: float,
color: str = 'black',
opacity: float = 1.0,
) -> None: ...
@classmethod
def unit(cls, color: str = 'black', opacity: float = 1.0) -> Circle: ...
class Box(Generic[T]):
def put(self, item: T) -> None: ...
def get(self) -> T: ...
@overload
def parse(x: int) -> int: ...
@overload
def parse(x: str) -> str: ...
Project layout
stubpy/
├── stubpy/ ← package (stdlib only, no runtime deps)
│ ├── context.py StubContext, AliasEntry, StubConfig, ExecutionMode
│ ├── diagnostics.py DiagnosticCollector, Diagnostic
│ ├── ast_pass.py ast_harvest, ASTSymbols
│ ├── symbols.py SymbolTable, StubSymbol hierarchy
│ ├── loader.py load_module
│ ├── aliases.py build_alias_registry
│ ├── imports.py scan / collect imports
│ ├── annotations.py dispatch-table annotation_to_str
│ ├── resolver.py resolve_params (3 strategies + pos-only normalisation)
│ ├── emitter.py generate_class / method / function / alias / overload stubs
│ └── generator.py generate_stub orchestrator (8-stage pipeline)
├── demo/ demo package used for integration tests
├── tests/ pytest suite (670+ tests)
├── docs/ Sphinx + Furo documentation
├── LICENSE
└── pyproject.toml
Contributing
git clone https://github.com/wzjoriv/stubpy.git
cd stubpy
py -3.11 -m venv .venv
.venv\Scripts\activate
pip install -e ".[dev]"
pytest
License
MIT © 2026 Josue N Rivera
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file stubpy-0.4.0.tar.gz.
File metadata
- Download URL: stubpy-0.4.0.tar.gz
- Upload date:
- Size: 90.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b171b20f067dd7bcf92b10c990061e1e3e59f569387c916e49e767c922e680bd
|
|
| MD5 |
3de9075f9303d7a583df9ac7d1b2a9ad
|
|
| BLAKE2b-256 |
31c9d610958a831ac5ccaef678716fbbfed4a2ab98100a480dd761c55a2ca290
|
File details
Details for the file stubpy-0.4.0-py3-none-any.whl.
File metadata
- Download URL: stubpy-0.4.0-py3-none-any.whl
- Upload date:
- Size: 51.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8a8ea8b6f36fe098217076c760795b900ae384e623404262537fb6e024fffc2b
|
|
| MD5 |
48ef08b5651a9d295d1d762b6cc10772
|
|
| BLAKE2b-256 |
dbd94c555d2caf60bf039e496a744141c7c904d15210bc03d611b1152ed63e74
|