• 
      

    Add the ability for functions to inherit another functions' parameters.

    Review Request #14507 — Created July 14, 2025 and submitted

    Information

    typelets
    main

    Reviewers

    One of the big missing gaps in the world of Python's type annotation
    support is a way for a function to reference another function's
    arguments in a signature. It's common for a function to take *args
    and/or **kwargs and pass them somewhere else, but doing this loses all
    typing.

    The "official" way to deal with this is to effectively make all
    arguments keyword-only and to define them in a function and in a
    TypedDict, and use that for all wrapping functions. This is good in
    many cases, but is not always a suitable approach.

    Instead, this change introduces some new utilities for typing the
    *args and **kwargs based on another function. The way it works is
    through a combination of a decorator and a "ParamsFrom" class.

    The "ParamsFrom" classes take in a callable, reference its ParamSpec,
    and mixes it into a signature in that class's __call__. This gives us
    a new ParamSpec that we can then reference in the decorator.

    The decorator takes in the "ParamsFrom" class as a
    Callable[TParams, Any]. That gives the decorator access to the
    modified signature. It can then return a variation on the decorated
    function that uses that signature.

    We complete that by fixing up the annotations and signature to use the
    right typing for the *args and **kwargs, allowing those to pass
    through.

    This has some initial startup cost (though minimal) and no runtime cost
    when calling functions.

    The end result is that any calls to the function will properly type the
    parameters going in, based on that signature.

    In practice, there are a few constraints to be aware of:

    1. ParamSpec doesn't do well with positional-only parameters. A
      function that has a self and then references positional-only
      parameters seems to reliably break when trying to match the type for
      the decorated function. This is solved with a # type: ignore, and
      does not appear to affect calls to the function.

    2. Function signatures need to be compatible between the
      decorated/signature and the referenced function. There's no going
      from keyword-only to positional-or-keyword, for example.

    3. Pylance/Pyright (and Visual Studio Code) have known issues around
      choosing the wrong docstrings when decorating a function and
      referencing another function as a parameter. This means that if
      hovering over a call to a function in any IDE using these as a basis
      for a LSP, we're going to get the wrong docstring for the function.
      There are many tickets/threads about this, but this is the most
      recent with any promising discussion:
      https://github.com/microsoft/pylance-release/issues/5840

    4. This does nothing useful in PyCharm, which doesn't handle ParamSpec
      fully. This notably is no worse than the behavior functions would
      have by default.

    There's thorough documentation within the codebase docs, but a guide to
    using this will be coming separately.

    This is all very experimental, but types correctly in every major type
    checker currently in use except for PyCharm. The hope is that some
    wonderful person will design a PEP that makes this entire change
    outdated.

    Unit tests pass on all supported versions of Python.

    Mypy, Pyright, and Pylance report zero typing issues.

    Summary ID
    Add the ability for functions to inherit another functions' parameters.
    One of the big missing gaps in the world of Python's type annotation support is a way for a function to reference another function's arguments in a signature. It's common for a function to take `*args` and/or `**kwargs` and pass them somewhere else, but doing this loses all typing. The "official" way to deal with this is to effectively make all arguments keyword-only and to define them in a function and in a `TypedDict`, and use that for all wrapping functions. This is good in many cases, but is not always a suitable approach. Instead, this change introduces some new utilities for typing the `*args` and `**kwargs` based on another function. The way it works is through a combination of a decorator and a "ParamsFrom" class. The "ParamsFrom" classes take in a callable, reference its `ParamSpec`, and mixes it into a signature in that class's `__call__`. This gives us a new `ParamSpec` that we can then reference in the decorator. The decorator takes in the "ParamsFrom" class as a `Callable[TParams, Any]`. That gives the decorator access to the modified signature. It can then return a variation on the decorated function that uses that signature. We complete that by fixing up the annotations and signature to use the right typing for the `*args` and `**kwargs`, allowing those to pass through. This has some initial startup cost (though minimal) and no runtime cost when calling functions. The end result is that any calls to the function will properly type the parameters going in, based on that signature. In practice, there are a few constraints to be aware of: 1. `ParamSpec` doesn't do well with positional-only parameters. A function that has a `self` and then references positional-only parameters seems to reliably break when trying to match the type for the decorated function. This is solved with a `# type: ignore`, and does not appear to affect calls to the function. 2. Function signatures need to be compatible between the decorated/signature and the referenced function. There's no going from keyword-only to positional-or-keyword, for example. 3. Pylance/Pyright (and Visual Studio Code) have known issues around choosing the wrong docstrings when decorating a function and referencing another function as a parameter. This means that if hovering over a call to a function in any IDE using these as a basis for a LSP, we're going to get the wrong docstring for the function. There are many tickets/threads about this, but this is the most recent with any promising discussion: https://github.com/microsoft/pylance-release/issues/5840 4. This does nothing useful in PyCharm, which doesn't handle `ParamSpec` fully. This notably is no worse than the behavior functions would have by default. There's thorough documentation within the codebase docs, but a guide to using this will be coming separately. This is all very experimental, but types correctly in every major type checker currently in use except for PyCharm. The hope is that some wonderful person will design a PEP that makes this entire change outdated.
    2d52a1960cff1a298e29f7cdcd6bea38e7aa87b1
    Description From Last Updated

    line too long (80 > 79 characters) Column: 80 Error code: E501

    reviewbotreviewbot

    line too long (80 > 79 characters) Column: 80 Error code: E501

    reviewbotreviewbot

    line too long (82 > 79 characters) Column: 80 Error code: E501

    reviewbotreviewbot

    line too long (81 > 79 characters) Column: 80 Error code: E501

    reviewbotreviewbot

    Can we fix this up to be autodoc_default_options? autodoc_default_flags is long deprecated.

    daviddavid

    Maybe add 314 while we're at it?

    daviddavid

    Can we put 2 blank lines between sections?

    daviddavid

    Here too.

    daviddavid

    The docstring for these says "Set to None to avoid looking for this argument" but these are typed as str. …

    daviddavid

    typo: Towner -> TOwner

    daviddavid

    This got cut off.

    daviddavid
    Checks run (1 failed, 1 succeeded)
    flake8 failed.
    JSHint passed.

    flake8

    chipx86
    david
    1. 
        
    2. docs/conf.py (Diff revision 2)
       
       
      Show all issues

      Can we fix this up to be autodoc_default_options? autodoc_default_flags is long deprecated.

    3. tox.ini (Diff revision 2)
       
       
      Show all issues

      Maybe add 314 while we're at it?

    4. tox.ini (Diff revision 2)
       
       
       
       
      Show all issues

      Can we put 2 blank lines between sections?

    5. tox.ini (Diff revision 2)
       
       
      Show all issues

      Here too.

    6. typelets/funcs.py (Diff revision 2)
       
       
       
      Show all issues

      The docstring for these says "Set to None to avoid looking for this argument" but these are typed as str. type_func_params_as types these as (str | None).

    7. typelets/funcs.py (Diff revision 2)
       
       
      Show all issues

      typo: Towner -> TOwner

    8. typelets/funcs.py (Diff revision 2)
       
       
       
       
      Show all issues

      This got cut off.

    9. 
        
    chipx86
    david
    1. Ship It!
    2. 
        
    chipx86
    Review request changed
    Status:
    Completed
    Change Summary:
    Pushed to main (3b79839)