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.
  • 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:

  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.2.tar.gz (117.7 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.2-py3-none-any.whl (68.0 kB view details)

Uploaded Python 3

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

Hashes for stubpy-0.5.2.tar.gz
Algorithm Hash digest
SHA256 40883f6ef4175d393e00b6c70c629b729cfab80a057d13ed4278d3962578a565
MD5 bd749144d9ced626cc7420a87fbad326
BLAKE2b-256 29fad2f8b1d61eb19edfb640acc4dbd36c82292e30cb2845ac7d59c6a86e6d3f

See more details on using hashes here.

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

Hashes for stubpy-0.5.2-py3-none-any.whl
Algorithm Hash digest
SHA256 3757a9a962168474a7d9ea103a527c058e466131235dd19e168488c15a3126a8
MD5 7f3709228aa7ed56176614efaf3feb4a
BLAKE2b-256 3875ce26684b2609afcaf881aeaa124defeff886894b1aa5013e37b4e6087d6e

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