typing icon indicating copy to clipboard operation
typing copied to clipboard

ParamSpec: bound, covariant, contravariant

Open sobolevn opened this issue 4 years ago • 8 comments

I found that right now ParamSpec allows three kw-only arguments: bound=None, covariant=False, contravariant=False. Just like PEP612 states:

The runtime should accept bounds and covariant and contravariant arguments in the declaration just as typing.TypeVar does, but for now we will defer the standardization of the semantics of those options to a later PEP.

What are the use-cases for this? How can ParamSpec can be bound to a value? Or how can it be covariant? It does not seem to be semantically valid.

Maybe we should remove these arguments? What do others think?

sobolevn avatar Jan 15 '22 23:01 sobolevn

Personally I was hopeful that bound would be useful for pass through functions at some point

Gobot1234 avatar Jan 15 '22 23:01 Gobot1234

@Gobot1234 can you please provide a short example?

sobolevn avatar Jan 15 '22 23:01 sobolevn

from typing import ParamSpec

P = ParamSpec("P", bound="actual")

def actual(x: int, y: float, z: str):
    ...

def pass_through(*args: P.args, **kwargs: P.kwargs):
    return actual(*args, **kwargs)

Ideally this would also work with classes (although I'm not sure what the signature would look like cause self is a thing) for things like super() calls.

Gobot1234 avatar Jan 15 '22 23:01 Gobot1234

I agree that ParamSpec should not allow a bound type, type constraints, or variance parameters. As you said, these are semantically invalid. FWIW, pyright already emits errors for these conditions.

@Gobot1234, in your example actual isn't a valid type, so this wouldn't work. Also, I don't understand why you would want to use a ParamSpec in this case. If you know the signature of a function, a ParamSpec provides no value. It adds unnecessary complexity and obfuscates the code. Plus, the bound in a TypeVar is meant to define an upper bound on the allowed type. It's not clear what an "upper bound" would mean for a ParamSpec, which represents a function signature. What would it mean to create a subtype of a function signature?

erictraut avatar Jan 16 '22 05:01 erictraut

Here's what the docs say:

class typing.ParamSpec(name, *, bound=None, covariant=False, contravariant=False) Parameter specification variables created with covariant=True or contravariant=True can be used to declare covariant or contravariant generic types. The bound argument is also accepted, similar to TypeVar. However the actual semantics of these keywords are yet to be decided.

Going to CC @mrkmndz and @gvanrossum as PEP authors 🙂 And @Fidget-Spinner as implementation author.

sobolevn avatar Jan 16 '22 07:01 sobolevn

@Gobot1234, in your example actual isn't a valid type, so this wouldn't work.

Maybe it would make more sense as bound (x: int, y: float, z: str) -> object (although you then lose info about where the signature is coming from)

Also, I don't understand why you would want to use a ParamSpec in this case. If you know the signature of a function, a ParamSpec provides no value. It adds unnecessary complexity and obfuscates the code.

It's fewer signatures to update and maintain

Plus, the bound in a TypeVar is meant to define an upper bound on the allowed type. It's not clear what an "upper bound" would mean for a ParamSpec, which represents a function signature. What would it mean to create a subtype of a function signature?

I'm not entirely sure about what it would mean either.

I have however found a way to implement what I'm looking for (apart from not handling self in classes):

def copy_params_from(copy_from: "(**P) -> object") -> "((...) -> R) -> (**P) -> R":
    def inner(actual: "(...) -> R") -> "(**P) -> R": return actual
    return inner

Gobot1234 avatar Jan 16 '22 09:01 Gobot1234

I agree that ParamSpec should not allow a bound type, type constraints, or variance parameters.

Is it too late to amend the PEP and remove the mention of these parameters? They could be re-introduced later once we figure out how (and if) they could work for ParamSpecs.

[...] Plus, the bound in a TypeVar is meant to define an upper bound on the allowed type. It's not clear what an "upper bound" would mean for a ParamSpec, which represents a function signature.

This sounds like a possible gap in PEP-483 and PEP-484 which do not describe what the type of a function definition is and how it participates in subtyping. If that was specified, then bound could actually make sense for ParamSpec.

superbobry avatar Jan 17 '22 13:01 superbobry

Is it too late to amend the PEP and remove the mention of these parameters?

They exist at runtime in Python 3.10, so backward compatibility means we can't remove them. Type checkers are free to reject code that uses these parameters, of course.

Note that PEP 646, which introduces another TypeVar variant, disallows the other TypeVar arguments: https://www.python.org/dev/peps/pep-0646/#variance-type-constraints-and-type-bounds-not-yet-supported.

JelleZijlstra avatar Jan 17 '22 15:01 JelleZijlstra