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).
  • Structured diagnostics — every pipeline stage records INFO, WARNING, and ERROR entries 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

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:

  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] [--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


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.0.tar.gz (100.4 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.0-py3-none-any.whl (58.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: stubpy-0.5.0.tar.gz
  • Upload date:
  • Size: 100.4 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.0.tar.gz
Algorithm Hash digest
SHA256 606b8048f20badbd9937a84f32f903af1e1393c08ea1ae7b056eefaf109f4873
MD5 125c0193d8389fe19d462934e7d26f55
BLAKE2b-256 38a6550fce9935db6dafd72fb51fcbaf300d081ab3352ee995814c56c608d23e

See more details on using hashes here.

File details

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

File metadata

  • Download URL: stubpy-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 58.4 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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 44417026f35667f82097ee5bdc37c7282dae91929ddd773243a2e0996c413617
MD5 d26682b2a5ff266111123d81c740bfb2
BLAKE2b-256 7c9ac685977055ec4be8aa550b9e6e98afc31b0edae1022923aab398c52f2a8d

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