GitHub - jorenham/unpy: Python stub backporter
Unified Python
Transpiles .pyi stubs from Python 3.13 to 3.10
Important
This project is in the alpha stage: You probably shouldn't use it in production.
Installation
Usage
$ unpy --help Usage: unpy [OPTIONS] SOURCE [OUTPUT] Arguments: SOURCE Path to the input .pyi file or '-' to read from stdin. [required] [OUTPUT] Path to the output .pyi file. Defaults to stdout. Options: --version Show the version and exit --diff Show the changes between the input and output in unified diff format --target [3.10|3.11|3.12|3.13] The minimum Python version that should be supported. [default: 3.10] --help Show this message and exit.
Examples
Some simple examples of Python 3.13 stubs that are backported to Python 3.10.
Imports
$ unpy --target 3.10 --diff examples/imports.pyi+++ - @@ -1,6 +1,4 @@ - from types import CapsuleType - from typing import override - from warnings import deprecated + from typing_extensions import CapsuleType, deprecated, override @deprecated("RTFM") class Spam: __pyx_capi__: dict[str, CapsuleType] @override def __hash__(self, /) -> int: ...
Note the alphabetical order of the generated imports.
Type Aliases
$ unpy --target 3.10 --diff examples/type_aliases.pyi+++ - @@ -1,7 +1,15 @@ from collections.abc import Callable + from typing import ParamSpec, TypeAlias, TypeVar + from typing_extensions import TypeAliasType, TypeVarTuple, Unpack - type Binary = bytes | bytearray | memoryview - type Vector[R: float] = tuple[R, ...] - type tciD[V, K] = dict[K, V] - type Things[*Ts] = tuple[*Ts] - type Callback[**Tss] = Callable[Tss, None] + _R = TypeVar("_R", bound=float) + _V = TypeVar("_V") + _K = TypeVar("_K") + _Ts = TypeVarTuple("_Ts") + _Tss = ParamSpec("_Tss") + + Binary: TypeAlias = bytes | bytearray | memoryview + Vector: TypeAlias = tuple[_R, ...] + tciD = TypeAliasType("tciD", dict[_K, _V], type_params=(_V, _K)) + Things: TypeAlias = tuple[Unpack[_Ts]] + Callback: TypeAlias = Callable[_Tss, None]
Note that TypeAlias cannot be used with tciD because the definition order of the
type parameters (at the left-hand side) does not match the order in which they are
accessed (at the right-hand side), and the backported TypeAliasType must be used
instead.
Functions
$ unpy --target 3.10 --diff examples/functions.pyi+++ - @@ -1,6 +1,11 @@ + _T = TypeVar("_T") + _S = TypeVar("_S", str, bytes) + _X = TypeVar("_X") + _Theta = ParamSpec("_Theta") + _Y = TypeVar("_Y") from collections.abc import Callable as Def - from typing import Concatenate as Concat + from typing import Concatenate as Concat, ParamSpec, TypeVar - def noop[T](x: T, /) -> T: ... - def concat[S: (str, bytes)](left: S, right: S) -> S: ... - def curry[X, **Theta, Y](f: Def[Concat[X, Theta], Y], /) -> Def[[X], Def[Theta, Y]]: ... + def noop(x: _T, /) -> _T: ... + def concat(left: _S, right: _S) -> _S: ... + def curry(f: Def[Concat[_X, _Theta], _Y], /) -> Def[[_X], Def[_Theta, _Y]]: ...
Generic classes and protocols
$ unpy --target 3.10 --diff examples/generics.pyi+++ - @@ -1,17 +1,25 @@ - from typing import Protocol, overload + from typing import Generic, Protocol, overload + from typing_extensions import TypeVar + + _T_contra = TypeVar("_T_contra", contravariant=True) + _T_co = TypeVar("_T_co", covariant=True) + _T = TypeVar("_T", infer_variance=True) + _D = TypeVar("_D") + _NameT = TypeVar("_NameT", infer_variance=True, bound=str) + _QualNameT = TypeVar("_QualNameT", infer_variance=True, bound=str, default=_NameT) class Boring: ... - class CanGetItem[T_contra, T_co](Protocol): - def __getitem__(self, k: T_contra, /) -> T_co: ... + class CanGetItem(Protocol[_T_contra, _T_co]): + def __getitem__(self, k: _T_contra, /) -> _T_co: ... - class Stack[T]: - def push(self, value: T, /) -> None: ... + class Stack(Generic[_T, _D]): + def push(self, value: _T, /) -> None: ... @overload - def pop(self, /) -> T: ... + def pop(self, /) -> _T: ... @overload - def pop[D](self, default: D, /) -> T | D: ... + def pop(self, default: _D, /) -> _T | _D: ... - class Named[NameT: str, QualNameT: str = NameT]: - __name__: NameT - __qualname__: QualNameT + class Named(Generic[_NameT, _QualNameT]): + __name__: _NameT + __qualname__: _QualNameT
Note how TypeVar is (only) imported from typing_extensions here, which wasn't the
case in the previous example. This is a consequence of the infer_variance parameter,
which has been added in Python 3.12.
Project goals
Here's the alpha version of a prototype of a rough sketch of some initial ideas for the
potential goals of unpy:
- Towards the past
- Get frustrated while stubbing scipy
- Transpile Python 3.13
.pyistubs to Python 3.10 stubs - Package-level analysis and conversion
- Tooling for stub-only project integration
- Use this in
scipy-stubs - Gradually introduce this into
numpy
- Towards the future
- Beyond Python:
$\text{Unpy} \supset \text{Python}$ - Language support & tooling for all
.pyprojects
- Beyond Python:
- Towards each other
- Unified typechecking: Fast, reasonable, and language-agnostic
Features
Tooling
- Target Python versions
-
3.13 -
3.12 -
3.11 -
3.10 -
3.9
-
- Language support
-
.pyi -
.py
-
- Conversion
- stdin => stdout
- module => module
- package => package
- project => project (including the
pyproject.toml)
- Configuration
-
--diff: Unified diffs -
--target: Target Python version, defaults to3.10 - Project-based config in
pyproject.tomlunder[tools.unpy] - ...
-
- Integration
- File watcher
- Pre-commit
- LSP
- UV
- VSCode extension
- (based)mypy plugin
- Project build tools
- Configurable type-checker integration
- Configurable formatter integration, e.g.
ruff format
- Performance
- Limit conversion to changed files
Stub backporting
- Python 3.13 => 3.12
- PEP 742
typing.TypeIs=>typing_extensions.TypeIs
- PEP 705
typing.ReadOnly=>typing_extensions.ReadOnly
- PEP 702
warnings.deprecated=>typing_extensions.deprecated
- PEP 696
- Backport PEP 695 type signatures with a default
typing.NoDefault=>typing_extensions.NoDefault
- Exceptions
asyncio.QueueShutDown=>builtins.Exceptionpathlib.UnsupportedOperation=>builtins.NotImplementedErrorqueue.ShutDown=>builtins.Exceptionre.PatternError=>re.error
- Typing
types.CapsuleType=>typing_extensions.CapsuleTypetyping.{ClassVar,Final}=>typing_extensions.{ClassVar,Final}when nested
- PEP 742
- Python 3.12 => 3.11
- Python 3.11 => 3.10
- PEP 681
typing.dataclass_transform=>typing_extensions.dataclass_transform
- PEP 675
typing.LiteralString=>typing_extensions.LiteralString
- PEP 673
typing.Self=>typing_extensions.Self
- PEP 655
typing.[Not]Required=>typing_extensions.[Not]Required
- PEP 654
builtins.BaseExceptionGroupbuiltins.ExceptionGroup
- PEP 646
typing.TypeVarTuple=>typing_extensions.TypeVarTupletyping.Unpack=>typing_extensions.Unpack*Ts=>typing_extensions.Unpack[Ts]withTs: TypeVarTuple
asyncioasyncio.TaskGroup
enumenum.ReprEnum=>enum.Enumenum.StrEnum=>str & enum.Enum
typingtyping.Any=>typing_extensions.Anyif subclassed (not recommended)
- PEP 681
- Generated
TypeVars- De-duplicate extracted typevar-likes with same name if equivalent
- Prefix the names of extracted typevar-likes with
_ - Rename incompatible typevar-likes with the same name (#86)
Simplification and refactoring
- Generic type parameters
- Convert
default=Anywithbound=Ttodefault=T - Remove
bound=Anyandbound=object - Infer variance of PEP 695 type parameters (#44)
- If never used, it's redundant (and bivariant) (#46)
- If constraints are specified, it's
invariant - If suffixed with
_co/_contra, it'scovariant/contravariant - If used as public instance attribute, it's
invariant - If only used as return-type (excluding
__init__and__new__), or for read-only attributes, it'scovariant - If only used as parameter-type, it's
contravariant - Otherwise, assume it's
invariant
- Convert
- Methods
- Default return types for specific "special method" (#55)
- Transform
selfmethod parameters to be positional-only
- Typing operators
-
type[S] | type[T]=>type[S | T] - Flatten & de-duplicate unions of literals
- Remove redundant union values, e.g.
bool | int=>int
-
Beyond Python
-
@sealedtypes (#42) - Unified type-ignore comments (#68)
- Set-based
Literalsyntax (#76) - Reusable method signature definitions (#97, #98)
- Type-mappings, a DRY alternative to
@overload - Intersection types (as implemented in basedmypy)
- Higher-kinded types (see python/typing#548)
- Inline callable types (inspired by PEP 677)