python / python/typing

Clarify the valid use locations of `typing.Concatenate`

Open
#2,140 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

topic: documentation
Dominant language
Python
Stars
1.8k
Forks
302
Avg merge
23h
Merged PRs (30d)
8

Description

In python/cpython#142965, it was reported that the documentation of typing.Concatenate is "incorrect" (documentation that originates back to python/cpython#24000).

@A5rocks, in your example, Concatenate[int, P_2] somewhat finally lands as a first argument to Callable, just indirectly.
This snippet previously defined:

P_2 = ParamSpec("P_2")

class X(Generic[T, P]):
  f: Callable[P, int]
  x: T

Similar example is included in the typing spec:

https://typing.python.org/en/latest/spec/generics.html#user-defined-generic-classes

class X[T, **P]:
    f: Callable[P, int]
    x: T

# (...)
def accept_concatenate[**P](x: X[int, Concatenate[int, P]]) -> str: ...  # Accepted

However, it seems that the current situation (confirmed with mypy, pyright and ty) is that you can use Concatenate in all valid locations of ParamSpec except directly in a Concatenate. I.e., you can't do Concatenate[int, Concatenate[str, P]]), but you can (besides passing Concatenate form as the first argument to Callable):

  1. Accumulate Concatenates as ParamSpecs
from collections.abc import Callable
from typing import Concatenate

type Y[**P] = Callable[Concatenate[int, P], None]
type X[**P] = Y[Concatenate[int, P]]

def foo(f: X[str]) -> None:
    reveal_type(f)
    # mypy: def (builtins.int, builtins.int, builtins.str)
    # pyright: (int, int, str) -> None
    # ty: (...) -> None
  1. Bind Concatenates to user-defined generics as ParamSpecs
class X[T, **P]:
    f: Callable[P, int]
    x: T

def accept_concatenate[**P](x: X[int, Concatenate[int, P]]) -> str: ...
  1. Use Concatenate as a type argument to tuple -- is this correct?
from typing import Concatenate

def c(t: tuple[Concatenate[int, ...]]) -> None:
    reveal_type(c)
    # mypy: def (t: tuple[[builtins.int, *Any, **Any]])
    # pyright: (t: tuple[Concatenate[int, ...]]) -> None
    # ty: def c(t: tuple[@Todo]) -> None

I've found this mostly by poking around -- I haven't analyzed the actual implementations (yet).
I'll continue to investigate this from these searches.

I think that the valid use locations of Concatenate should be clarified in the typing spec and then in the CPython docs.

Is there anything else I overlooked? CC @JelleZijlstra @AlexWaygood

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the typing spec's user-defined generic classes section and the CPython documentation for typing.Concatenate, then compare the examples and the reported behavior from mypy, pyright, and ty. Review the linked Concatenate search notes and determine which use locations are valid; done means the typing spec and CPython docs clearly describe the supported forms and the nested-Concatenate restriction.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
documentation
Issue type
Documentation
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.