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, package batch generation, 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 / Generic / overload — TypeVar, TypeAlias, NewType, ParamSpec, and TypeVarTuple declarations are re-emitted verbatim.
Generic[T]bases are preserved via__orig_bases__. 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[...], and mixed unions. - Cross-file imports — base classes and annotation types from other local modules are re-emitted in the
.pyiheader automatically. - Package batch generation —
generate_package()recursively stubs a whole directory tree, mirrors the structure, and creates__init__.pyimarkers for every sub-package. - Configuration file —
stubpy.tomlor[tool.stubpy]inpyproject.tomlcontrols all options; CLI flags override file values. - Typing style —
"modern"(default, PEP 604X | None) or"legacy"(Optional[X]) output. - Execution modes —
RUNTIME(default),AST_ONLY(no module execution),AUTO(runtime with graceful fallback). - Type alias detection — explicit
Name: TypeAlias = ..., bareName = int | float, subscripted generics, known type names, and Python 3.12+type Name = ...(PEP 695) are all detected and emitted correctly. - # stubpy: ignore — place this comment at the top of any source file to exclude it from stub generation entirely.
- Structured diagnostics — every pipeline stage records
INFO,WARNING, andERRORentries rather than swallowing exceptions silently. - Zero runtime dependencies — stdlib only.
Installation
pip install stubpy
# or
uv add stubpy
Requires Python 3.10+.
Quickstart
Single file
stubpy path/to/module.py # writes module.pyi alongside source
stubpy path/to/module.py -o stubs/ # custom output path
stubpy path/to/module.py --print # also print to stdout
Multiple files
stubpy a.py b.py c.py # stubs written alongside each source
stubpy src/*.py # shell glob expansion
stubpy module.py mypackage/ # mix files and directories
Whole package
stubpy mypackage/ # stubs written alongside source files
stubpy mypackage/ -o stubs/ # stubs written to stubs/
stubpy mypackage/ --typing-style legacy # use Optional[X] instead of X | None
Configuration file
Place a stubpy.toml in the project root (or add [tool.stubpy] to pyproject.toml):
# stubpy.toml
include_private = false
typing_style = "modern" # "modern" (X | None) | "legacy" (Optional[X])
output_dir = "stubs"
exclude = ["**/test_*.py", "docs/conf.py"]
All flags have CLI equivalents; CLI flags override file values.
How it works
generate_stub(filepath)
│
├─ 1. loader load_module() → module, path, name
│ └─ (skipped in AST_ONLY; warning+fallback in AUTO)
├─ 2. ast_pass ast_harvest() → ASTSymbols
├─ 3. imports scan_import_statements() → import_map
├─ 4. aliases build_alias_registry() → ctx populated
├─ 5. symbols build_symbol_table() → 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() → header
│ collect_special_imports()
│ collect_cross_imports()
└─ 8. write .pyi written to disk
generate_package(package_dir, output_dir)
└─ for each .py file: generate_stub(...)
└─ ensure __init__.pyi for each sub-package
resolve_params uses three strategies in order:
- No variadics — return own parameters unchanged.
cls()detection — AST-detectcls(..., **kwargs)in classmethods; resolve againstcls.__init__.- MRO walk — collect concrete parameters from each ancestor until all variadics are resolved.
POSITIONAL_ONLYparams absorbed by**kwargsare promoted toPOSITIONAL_OR_KEYWORD.
CLI reference
usage: stubpy [-h] [-o PATH] [--print] [--include-private] [--verbose]
[--strict] [--typing-style {modern,legacy}]
[--execution-mode {runtime,ast_only,auto}] [--no-config]
path
positional arguments:
path Python source file (.py) or package directory
optional arguments:
-o PATH Output .pyi path (file) or root directory (package)
--print Print generated stub to stdout (file mode only)
--include-private Include symbols starting with _
--verbose Print all diagnostics (INFO/WARNING/ERROR) to stderr
--strict Exit 1 if any ERROR diagnostic was recorded
--typing-style STYLE Output style: modern (X | None) or legacy (Optional[X])
--execution-mode MODE runtime | ast_only | auto
--no-config Ignore stubpy.toml / pyproject.toml
Python API
from stubpy import generate_stub, generate_package, load_config, StubContext, StubConfig
# Single file
content = generate_stub("mymodule.py")
content = generate_stub("mymodule.py", "stubs/mymodule.pyi")
# Whole package
result = generate_package("mypackage/", "stubs/")
print(result.summary()) # "Generated 12 stubs, 0 failed."
# Custom config
cfg = StubConfig(typing_style="legacy", exclude=["**/migrations/*.py"])
result = generate_package("myapp/", "stubs/", config=cfg)
# Load config from file (stubpy.toml or pyproject.toml)
cfg = load_config(".")
result = generate_package("mypackage/", config=cfg)
Extended public API
# Context and configuration
from stubpy import StubContext, StubConfig, ExecutionMode, AliasEntry
# 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 (public for extension)
from stubpy import (
generate_class_stub, generate_function_stub, generate_variable_stub,
generate_alias_stub, generate_overload_group_stub,
)
# Config file support
from stubpy import find_config_file, load_config
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, 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 (MRO backtracing + pos-only normalisation)
│ ├── emitter.py generate_class / method / function / alias / overload stubs
│ ├── generator.py generate_stub + generate_package orchestrator
│ ├── config.py find_config_file, load_config (TOML parsing)
│ └── __main__.py CLI entry point
├── demo/ demo package used for integration tests
├── tests/ pytest suite (730+ tests)
│ ├── test_annotations.py
│ ├── test_ast_pass.py
│ ├── test_config.py
│ ├── test_context.py
│ ├── test_diagnostics.py
│ ├── test_emitter.py
│ ├── test_imports.py
│ ├── test_integration.py
│ ├── test_loader.py
│ ├── test_module_symbols.py
│ ├── test_resolver.py
│ ├── test_special_classes.py
│ └── test_symbols.py
├── docs/ Sphinx + Furo documentation
├── LICENSE
└── pyproject.toml
Development setup
git clone https://github.com/wzjoriv/stubpy.git
cd stubpy
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
pytest
Build the docs:
pip install -e ".[docs]"
cd docs && make html
# open docs/_build/html/index.html
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.5.2.tar.gz.
File metadata
- Download URL: stubpy-0.5.2.tar.gz
- Upload date:
- Size: 117.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
40883f6ef4175d393e00b6c70c629b729cfab80a057d13ed4278d3962578a565
|
|
| MD5 |
bd749144d9ced626cc7420a87fbad326
|
|
| BLAKE2b-256 |
29fad2f8b1d61eb19edfb640acc4dbd36c82292e30cb2845ac7d59c6a86e6d3f
|
File details
Details for the file stubpy-0.5.2-py3-none-any.whl.
File metadata
- Download URL: stubpy-0.5.2-py3-none-any.whl
- Upload date:
- Size: 68.0 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 |
3757a9a962168474a7d9ea103a527c058e466131235dd19e168488c15a3126a8
|
|
| MD5 |
7f3709228aa7ed56176614efaf3feb4a
|
|
| BLAKE2b-256 |
3875ce26684b2609afcaf881aeaa124defeff886894b1aa5013e37b4e6087d6e
|