Skip to main content

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.

PyPI Python License: MIT

Features

  • **kwargs backtracing — walks the entire MRO to expand **kwargs into concrete, named parameters at every inheritance level.
  • cls() detection@classmethod methods that forward **kwargs into cls(...) are resolved against cls.__init__, not MRO siblings.
  • Typed *args preserved — explicitly annotated *args (e.g. *elements: Element) always survive the resolution chain.
  • Positional-only / separatordef f(a, b, /, c) stubs correctly emit the PEP 570 / separator. Parent positional-only params absorbed by **kwargs are promoted to POSITIONAL_OR_KEYWORD to 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 @overload variant gets its own stub; the concrete implementation is suppressed per PEP 484.
  • Type-alias preservationtypes.Length stays types.Length rather than expanding to str | float | int. Works inside Optional[...], tuple[...], list[...], and mixed unions.
  • Cross-file imports — base classes and annotation types from other local modules are re-emitted in the .pyi header automatically.
  • Package batch generationgenerate_package() recursively stubs a whole directory tree, mirrors the structure, and creates __init__.pyi markers for every sub-package.
  • Configuration filestubpy.toml or [tool.stubpy] in pyproject.toml controls all options; CLI flags override file values.
  • Typing style"modern" (default, PEP 604 X | None) or "legacy" (Optional[X]) output.
  • Execution modesRUNTIME (default), AST_ONLY (no module execution), AUTO (runtime with graceful fallback).
  • Type alias detection — explicit Name: TypeAlias = ..., bare Name = 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, and ERROR entries rather than swallowing exceptions silently.
  • TypedDict / Enum / dataclass stubs — each class form gets a clean, correct stub without leaking internal implementation details.
  • Enum defaults rendered correctlyClassName.MEMBER form, not the unreadable repr().
  • NamedTuple extra methods@property and ordinary methods on NamedTuple subclasses are preserved.
  • Glob expansionstubpy "src/*.py" works even without shell expansion.
  • --include-docstrings — embed docstrings in stub bodies.
  • Custom annotation handlersregister_annotation_handler() lets you extend the dispatch table.
  • 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/ --union-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
union_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:

  1. No variadics — return own parameters unchanged.
  2. cls() detection — AST-detect cls(..., **kwargs) in classmethods; resolve against cls.__init__.
  3. MRO walk — collect concrete parameters from each ancestor until all variadics are resolved. POSITIONAL_ONLY params absorbed by **kwargs are promoted to POSITIONAL_OR_KEYWORD.

CLI reference

usage: stubpy [-h] [-o PATH] [--print] [--include-private] [--verbose]
              [--strict] [--union-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
  --union-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(union_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

Documentation

Full documentation including per-symbol API pages, example walkthroughs, and the "How it works" guide is available at: https://wzjoriv.github.io/stubpy

Every public function and class has its own page with:

  • Full parameter descriptions (from docstrings)
  • Usage examples
  • Source link

License

MIT © 2026 Josue N Rivera

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

stubpy-0.5.3.tar.gz (125.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stubpy-0.5.3-py3-none-any.whl (71.2 kB view details)

Uploaded Python 3

File details

Details for the file stubpy-0.5.3.tar.gz.

File metadata

  • Download URL: stubpy-0.5.3.tar.gz
  • Upload date:
  • Size: 125.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.0

File hashes

Hashes for stubpy-0.5.3.tar.gz
Algorithm Hash digest
SHA256 07b8e3d4fdd992a2df13ec5fe1c4912bf21dfac7ecc7993f58139df525f115f4
MD5 dfa482d9029c15757df372b78ee4a6ac
BLAKE2b-256 f9d86a96a8c58b46a545abe98ebcea24967a9ca10de6b290aa3ef773df81e6ce

See more details on using hashes here.

File details

Details for the file stubpy-0.5.3-py3-none-any.whl.

File metadata

  • Download URL: stubpy-0.5.3-py3-none-any.whl
  • Upload date:
  • Size: 71.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.0

File hashes

Hashes for stubpy-0.5.3-py3-none-any.whl
Algorithm Hash digest
SHA256 707b98c7b78f7a5f49aebe651bd3f726b5fcaca662b8fabc292a431b452aba6e
MD5 0b88f413b409120996e5d281eed92215
BLAKE2b-256 7b1e0e520d85fb4ad15f55fcbb47f880ab9f0a88e5218a31e4279922126dc9ef

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page