I think that in addition to theoretical purity we need to keep usability in mind.
The original “asymmetric” tuple rules are designed to catch the most errors with the fewest false positives. It just doesn’t happen very often that you have a tuple of unknown size (to the checker) where the programmer knows it’s got a specific size and they want to pass it as such. If I were in that position I’d expect the need for a cast.
OTOH a function that says it takes a tuple of any size should be happy with a tuple of a given size. (Assuming the element types match!)
Doesn’t the same reasoning apply to tensor shape?
For the *args example the usability equation is just a bit different.
IOW, the saying about consistency and hobgoblins applies here.
On Sun, Feb 14, 2021 at 05:31 Matthew Rahtz via Typing-sig < email@example.com> wrote:
I did some more thinking about the `Tensor[Any, ...]` issue.
To recap, we said in the tensor typing meeting that although the following should clearly work fine...
def foo(x: Tensor): ... x: Tensor[Height, Width] foo(x)
...it's unclear whether the following should work...
def bar(y: Tensor[Height, Width]): ... y: Tensor foo(y)
...because of the fact that the following example does *not* work in Mypy, Pyre or Pyright:
def baz(z: Tuple[int, str]): ... z: Tuple foo(z)
The actual test code I'm using is:
from typing import Tuple def get_tuple() -> Tuple: return ('dummy_value',) def baz(z: Tuple[int, str]): ... z = get_tuple() reveal_type(z) baz(z)
foo.py:9: note: Revealed type is 'builtins.tuple[Any]' foo.py:10: error: Argument 1 to "baz" has incompatible type "Tuple[Any, ...]"; expected "Tuple[int, str]"
foo.py:9:0 Revealed type [-1]: Revealed type for `z` is `typing.Tuple[typing.Any, ...]`. foo.py:10:4 Incompatible parameter type : Expected `Tuple[int, str]` for 1st positional only parameter to call `baz` but got `typing.Tuple[typing.Any, ...]`.
9:13 - info: Type of "z" is "Tuple[Unknown, ...]" 10:5 - error: Argument of type "Tuple[Unknown, ...]" cannot be assigned to parameter "z" of type "Tuple[int, str]" in function "baz" Tuple size mismatch; expected 2 but received indeterminate number (reportGeneralTypeIssues)
(Pytype is fine with it, but not sure whether that's intentional.)
That seemed pretty conclusive, so based on Pyright's very helpful error message, I tried writing this up in the PEP, and my understanding of the rule is:
- If an *arbitrary* number of type parameters are expected, then it *is*
valid to pass a *specific* number of type parameters.
- If a *specific* number of type parameters are expected, then it is *not*
valid to pass an *arbitrary* number of type parameters.
But that seems like kind of an ad-hoc rule to me, and left me feeling kind of dissatisfied. As far as I can tell, they're both unsound, so why should it work one way, but not the other? To see if there might be any wiggle room, I wondered what would happen in this analogous situation:
from typing import Tuple def get_tuple() -> Tuple: return ('dummy_value',) def baz(arg1, arg2): ... z = get_tuple() reveal_type(z) baz(*z)
Mypy, Pyre and Pyright are all happy with this example! Hah!
So basically, I want to argue that, given the inconsistency between this example and the type parameter example, we should fix the inconsistency and make both cases behave in the same way, and I think it makes more sense to standardise on it working 'both ways' (`Tuple` is valid for `Tuple[int, str]`, and `Tuple[int, str]` is valid for `Tuple`).
Pradeep, Eric, what do you think?
On Sun, 7 Feb 2021 at 12:11, Matthew Rahtz firstname.lastname@example.org wrote:
I just realized there's a situation where bare generic types are
important: when evolving a codebase, making certain types that weren't generic before generic.
Yeah, this is exactly the situation that I imagine we'll be in with array types: *hopefully* (at least in my personal ideal world) we'd be able to persuade library authors to make the existing types like `tf.Tensor`, ` np.ndarray` etc generic in shape.
So I'm not in favor of saying that `Tensor` is the only legitimate way
to specify "a `Tensor` whose type arguments can be anything". If we think that concept is needed, then we should support `Tensor[Any, ...]`.
The idea I had in mind worked the opposite way around: it's less that ` Tensor` should be the only legitimate way of specifying arbitrary parameters, and more that arbitrary type parameters should be what ` Tensor` means, in order to support gradual typing if `Tensor` becomes generic. For cases where the user wants to deliberately specify "a Tensor of any shape", I'm imagining they'd do something like:
Shape = TypeVarTuple('Shape') def pointwise_multiply(x: Tensor[*Shape], y: Tensor[*Shape]) -> Tensor[*Shape]: ...
This would specify that `x` and `y` can be an arbitrary shape, but they should be the same shape, and that shape would also be the shape of the returned `Tensor`.
(If the user instead wanted to say that `x` and `y` could be arbitrary, different shapes, they would use different `TypeVarTuple` instances. This would, of course, potentially rely on the type-checker being ok with only a single usage of a `TypeVarTuple` in a function signature. From a quick skim of the analogous discussion about `TypeVar` in typing-sig a few months ago, I get the impression the jury is still out on whether that should be an error. But luckily, I think this use case is likely to be rare enough that we can ignore that issue for now. I can't think of any specific functions off the top of my head which would take arbitrary arrays of different shapes.)
I consider it a bug in mypy that it accepts `Tensor[()]`.
Interesting - do you say this mainly on the basis that since it causes a runtime error, it should also be a type error? To me, it feels more intuitive that this shouldn't be an error, based on the argument that ` Tensor` behaves like `Tensor[Any]`, and (without thinking about it too hard - i.e. not thinking about how the variance should work) `Tensor[Any]` seems like it should be compatible with `Tensor[()]`.
Interestingly, mypy does emit an error if you try to pass other illegal
values as a type argument (like `Tensor`).
I was surprised by this, so I did a bit more experimenting. Mypy does indeed error for me too on `Tensor`, but that was only because of the lack of `Literal`. The following two examples *do* type-check fine for me with Mypy:
x: Tensor[Literal] = Tensor() foo(x) x: Tensor[int] = Tensor() foo(x)
Having said all that -
I don't think we should promote the use of "bare" generic types —
those with no type arguments. That's typically indicative of an error on the programmer's part. Pyright accepts them, but it flags it as an error when "strict" mode is enabled.
This makes a lot of sense to me and I definitely think that such a flag should continue to exist and be prominently advertised. With the case of ` Tensor`, too, it would be super helpful to have the type-checker error with "Hey, you haven't specified what shape this `Tensor` should be!"
On Thu, 4 Feb 2021 at 18:15, Guido van Rossum email@example.com wrote:
On Wed, Feb 3, 2021 at 12:41 PM Guido van Rossum firstname.lastname@example.org wrote:
On Wed, Feb 3, 2021 at 11:15 AM Eric Traut email@example.com wrote:
I don't think we should promote the use of "bare" generic types — those with no type arguments. That's typically indicative of an error on the programmer's part. Pyright accepts them, but it flags it as an error when "strict" mode is enabled. This check has been really useful in helping developers to fix problems in their code, and I don't want to water it down. So I'm not in favor of saying that `Tensor` is the only legitimate way to specify "a `Tensor` whose type arguments can be anything". If we think that concept is needed, then we should support `Tensor[Any, ...]`. As I said, adding support for open-ended tuples will add yet more complexity to the specification and implementation of this PEP, but maybe it's required to meet all of the intended use cases.
Agreed. I think they are mostly a legacy feature.
I just realized there's a situation where bare generic types are important: when evolving a codebase, making certain types that weren't generic before generic. An real-world example is the stdlib Queue type -- this started its life as a non-generic class (in typeshed) and at some point it was made generic. During the time it was non-generic, user code was annotated with things like `(q: Queue)`, and it would be a problem to fault all that code immediately as being non-compliant. So the interpretation of this as `Queue[Any]` makes sense. (Of course a flag exists to make this an error, for users who want to update all their code to declare the item type of their queues.)
So it's not that bare generics themselves are a legacy feature -- but the feature is primarily important for situations where legacy code is being checked.
-- --Guido van Rossum (python.org/~guido) *Pronouns: he/him **(why is my pronoun here?)* http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-change-the-world/ _______________________________________________ Typing-sig mailing list -- firstname.lastname@example.org To unsubscribe send an email to email@example.com https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: firstname.lastname@example.org
Typing-sig mailing list -- email@example.com To unsubscribe send an email to firstname.lastname@example.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: email@example.com