Skip to content

Typing: improve error messages for invalid ParamSpec substitutions #102725

Description

@AlexWaygood

Feature or enhancement

PEP-612 is a complex typing PEP that introduced some complex rules, both for static type checkers and for CPython at runtime. It can be hard to remember the intricacies of all these rules -- both for users and for typing maintainers.

One thing that might help here is if we had better error messages in situations where the runtime raises exceptions. For example, in the following snippet, the runtime is arguably correct in raising TypeErrors -- by the rules laid out in PEP-612, both of these are invalid substitutions for two reasons:

  1. Parameters can only be prepended to a ParamSpec in a parameters list using typing(_extensions).Concatenate.
  2. Appending parameters to a ParamSpec in a parameters list is disallowed.

However, the error messages don't mention either of these things, and are in fact pretty baffling:

Running PGUpdate|x64 interpreter...
Python 3.12.0a6+ (heads/main:12226bec25, Mar 10 2023, 17:32:23) [MSC v.1932 64 bit (AMD64)] on win32
>>> import typing as t, collections.abc as c
>>> P, T = t.ParamSpec("P"), t.TypeVar("T")
>>> c.Callable[P, T][[P, str], bool][int]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<frozen _collections_abc>", line 501, in __getitem__
  File "<frozen _collections_abc>", line 456, in __new__
TypeError: Callable must be used as Callable[[arg, ...], result].
>>> class MyCallable(t.Generic[P, T]): ...
...
>>> MyCallable[P, T][[P, str], bool][int]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\alexw\coding\cpython\Lib\typing.py", line 354, in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
  File "C:\Users\alexw\coding\cpython\Lib\typing.py", line 1403, in __getitem__
    raise TypeError(f"{self} is not a generic class")
TypeError: __main__.MyCallable[[~P, str], bool] is not a generic class

Previous discussion

This issue is a followup to some points raised in:

Cc. @sobolevn

Activity

  1. added
    type-featureA feature request or enhancement
    stdlibStandard Library Python modules in the Lib/ directory
    3.12only security fixes
    on Mar 15, 2023
  2. sobolevn commented on Apr 11, 2023

    @sobolevn
    Member

    Update: turns out the later substitution does not raise an error anymore on main:

    import typing as t, collections.abc as c
    P, T = t.ParamSpec("P"), t.TypeVar("T")
    
    class MyCallable(t.Generic[P, T]): ...
    
    print(MyCallable[P, T][[P, str], bool][int])
    # __main__.MyCallable[[[int], str], bool]

    Not good!

    I think that this error is helpful and should be restored with a better message.

  3. self-assigned this
    on Apr 11, 2023
  4. sobolevn commented on Apr 11, 2023

    @sobolevn
    Member

    We can now even substitute T with [P, int]:

    import typing as t, collections.abc as c
    P, T, T1 = t.ParamSpec("P"), t.TypeVar("T"), t.TypeVar("T1")
    
    class MyCallable(t.Generic[T1, T]): ...
    
    print(MyCallable[T, T1][[P, str], bool])
    # __main__.MyCallable[[~P, <class 'str'>], bool]

    Not good at all!

  5. sobolevn commented on Apr 11, 2023

    @sobolevn
    Member

    Should we fix it? It will require quite a bit of work to get all the things right.
    I can do it if we need it. Or we can keep this up to a user to control.

  6. AlexWaygood commented on Apr 11, 2023

    @AlexWaygood
    MemberAuthor

    If it can be fixed without excessively complicating the code of typing.py, and if we can be 100% sure that we're not introducing false-positive errors, then I'll be happy to review a PR. But our general position is that it's better for the runtime to err on the side of leniency, and leave it to type checkers to point out invalid uses of typing constructs. We've had lots of situations in the past where strict checks at runtime have made typing_extensions backports complicated, or interfered with people using type annotations in unusual/experimental ways. Therefore, I view these "false negatives" at runtime as being quite low-priority concerns.

  7. sobolevn commented on Apr 11, 2023

    @sobolevn
    Member

    Let's start with the second example.
    The change itself is quite easy:

                if (isinstance(old_arg, ParamSpec)
                        and isinstance(new_arg, tuple)
                        and any(isinstance(na, ParamSpec) for na in new_arg)):
                    raise TypeError(
                        'Cannot replace a ParamSpec with '
                        'a tuple containing other ParamSpec, '
                        'use Concatenate[] instead'
                    )
                elif self.__origin__ == collections.abc.Callable and isinstance(new_arg, tuple):

    But, it has several flaws.

    1. collections.abc.Callable does not do the same
    2. It solves a very specific problem most users don't have
    3. Type-checker can catch this just fine: https://mypy-play.net/?mypy=latest&python=3.11&gist=627d3f8590da64c7c9e077067fd1b8e8
    main.py:6: error: Invalid location for ParamSpec "P"  [valid-type]
    main.py:6: note: You can use ParamSpec as the first argument to Callable, e.g., 'Callable[P, int]'
    

    So, I think we should leave this as-is.

    Now, let's get back to your first example.

    >>> import typing as t, collections.abc as c
    >>> P, T = t.ParamSpec("P"), t.TypeVar("T")
    >>> c.Callable[P, T][[P, str], bool][int]
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<frozen _collections_abc>", line 501, in __getitem__
      File "<frozen _collections_abc>", line 456, in __new__
    TypeError: Callable must be used as Callable[[arg, ...], result].

    It is also problematic, because right now typing.Callable does not produce this error:

    >>> import typing as t, collections.abc as c
    >>> P, T = t.ParamSpec("P"), t.TypeVar("T")
    >>> t.Callable[P, T][[P, str], bool][int]
    typing.Callable[[int, str], bool]

    Synchronizing them will be a huge pain.
    Because the exception happens on [int] substitution. But, we have to make an invalid substitution here c.Callable[P, T][[P, str], bool] to be able to reach this state.

    So, I think we should just close this issue.
    There's nothing that can be easily done to solve a hypothetical issue.

  8. removed their assignment
    on Apr 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.12only security fixesstdlibStandard Library Python modules in the Lib/ directorytopic-typingtype-featureA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions