python / typing

Python static typing home. Hosts the documentation and a user help forum.
https://typing.readthedocs.io/
Other
1.59k stars 234 forks source link

Add an exception to PEP 484 about meaning of generics without arguments #300

Closed ilevkivskyi closed 8 years ago

ilevkivskyi commented 8 years ago

Currently, PEP 484 says that bare List is equivalent to List[Any], etc. However, I think that this rule should be changed for type aliases like re.Match, re.Pattern, etc. For such cases bare re.Match should be equivalent to re.Match[AnyStr].

@gvanrossum If you agree, then you can assign this issue to me.

gvanrossum commented 8 years ago

But AnyStr is a type variable. I don't think we want to default to something that has a type variable.

Now, if we had a generic class whose type variable has an upper bound (e.g. TypeVar('T', bound=C) or value constraints (e.g. TypeVar('T', str, bytes)), it's perhaps reasonable that the default is not exactly Any but that upper bound or the union of the constraints. And for these special cases Match etc. the same could apply. Except that a union may be awkward to deal with -- if someone wrote Match out of laziness and then proceeded to use it as if they had written Match[str] they currently can get away with this. I can't decide whether I like that strictness or not...

ilevkivskyi commented 8 years ago

@gvanrossum Sorry, I was not precise, I meant exactly what you said. But now on second thought I also started to doubt whether we need such strictness. Maybe we should add a short note, like "A type checker could make exceptions from this rule for generics defined with constrained/bounded type variables"?

JukkaL commented 8 years ago

I agree with Guido that AnyStr is not a good default.

Using some sort of an upper bound is not a good idea either, because it would result in wider/more restrictive types and may require code to use isinstance tests to do anything useful with values of such types, which is the opposite of what Any does, which is to make it more flexible and never require any type checks. The property that Any behaves kind of like both the supertype and subtype of everything is important. I can give examples if you aren't convinced :-)

If we had the concept of an 'unsafe' union, we could use Match[UnsafeUnion[str, bytes]] as the meaning of Match. Unsafe unions have been discussed before, but the idea is that a type checker would allow an operation if it's supported by some union item type, whereas for normal unions it has to be supported for all types. Regular unions wouldn't really work either because it would actually be more restrictive than, say, Match[str].

[Here's some informal discussion that might make this clearer: For a value, a wider or larger type is more restrictive, because it supports fewer operations. For example, object is the widest ordinary type, and an expression (not lvalue) with type object only lets you do a few things with it such as str(x). For a function argument, a wider type is more general/permissive from the caller's point of view, and an object argument accepts anything, and similarly for lvalues. So types act differently in these two contexts. Match types can be used both for values and function arguments, and the aim is to make this type permissive in both contexts, similar to how typing.List (List[Any]) is permissive. By defaulting to re.Match[Any], it is permissive both in an argument/lvalue type context and a value context.]

ilevkivskyi commented 8 years ago

@JukkaL Thanks for a detailed answer! Indeed, you convinced me that there is a flaw in may logic: I was not thinking about generic types in contravariant positions, and those are clearly important. Closing this issue.