Thanks for taking time out of your vacation to reply!
** I would really like a response from Łukasz, the PEP author. **
Greetings from Valletta, Malta 🇲🇹 :-) I'm on vacation here with no laptop, will be more useful for you next week once I'm back.
No worries. In the meantime I'll prepare a PR with the changes that you've approved below.
- The proxy type will have a name: types.GenericAlias.
- list == list[str] should be false.
- isinstance([1, 2], list[int]) should raise TypeError.
My thinking was that I'd like to let the user parametrize types in cases where this is impossible today. This goes beyond object instantation but also when you are passing a type as an argument to a function where it's used as a factory, a converter, a validator, and so on.
Of course the PEP example looks and is deliberately) wrong: isinstance([1, 2, 3], list[str]) returns True! But this looks less controversial when declaring the parametrized type and doing the isinstance check are performed in different places. The type is declared by the user, the check is performed by the framework.
I think that's a useful capability. The proxy was meant to be thin and as transparent as possible. Raising TypeError here arguably goes in the direction of runtime type checking. The user error in the example in the PEP should be caught by static type checking instead.
I still disagree. The authors of PEP 484 and typing.py have thought about this quite a bit and decided that isinstance(x, typing.List[int]) should raise TypeError (regardless of the type of x). Honestly allowing it makes it *seems* as if runtime type checking might work, leading to disappointment.
- Make re.Pattern, re.Match, io.IO generic.
- I don't know whether we should make typing.List be the same object as builtins.list or not (and ditto for all generic classes in typing).
To clarify: you mean making typing.py use the new capabilities of the language starting with 3.9?
Sort of. For List, I was just thinking of replacing the line
List = _alias(list, T, inst=False)
List = list
Is it worth the compatibility risk? With the annonations future-import it doesn't really matter. We could make that change when the import becomes the default.
I'm happy not to touch typing.py for now.
- I'm stuck making type[int] work (this is needed so builtins.type[x] works like typing.Type[x]).
I see, disallowing indexing on subtypes is hard (impossible?) to do. type(int) means something else so we cannot use it as an alternative here.
If you prove this to be unfeasible to implement, we can either postpone this part until a better solution materializes, or use a different name for this. I need to think about this but one bad idea I have is to allow "class[int]" in the PEG parser. Those are annotations, the point of this would be very clear to the user.
No parser changes, please. If it's going to have a different name, we might as well say "for this use typing.Type". After all there are plenty of things in typing.py for which we're not going to offer alternatives (e.g. Any, Union, TypeVar, Generic, cast, final/Final, Protocol, Literal, SupportsAbs, ...).
1. We should rename __parameters__ to __args__, there's no reason to deviate from typing.py here.
I found "args" to be a busily overloaded name. We have argparse, we have *args and **kwargs, and so on. Those are parametrized types so they use parameters. But you're right.
Keeping __args__ might make it easier to replace typing.List with builtins.list in the future though. And:
2. We can relatively easily add a new __parameters__ attribute (lazily computed) that returns only the subset of __args__ that are type vars, and make it so that e.g. dict[str, T][int] works (and returns dict[str, int]) and list[int][int] is disallowed. This is what __parameters__ means in typing.py. It will fix 2 out of the 3 current test_typing.py failures.
👍🏻 I like this.
3. We *could* easily change GenericAlias.__call__ to set __orig_class__ on the new instance if it works, ignoring the error (e.g. if the class uses __slots__ or overrides __setattr__), but I see a downside: it adds an extra entry to self.__dict__ for each instance. While typing.py currently does this (and there's a test for it), I think it's a high cost to pay for making a class generic. And the purpose of `__orig_class__` is questionable -- why have it at all? (This is the third failure in test_typing.py -- if we decide not to do this, we should disable the failing parts of that test.)
Let's leave it out. The point about paying the price in __dict__ is good but the most important reason to not include it is that In general a developer cannot depend on __orig_class__ being there because most object instantiations won't be from parametrized types.
This is especially true for builtins. At best this may serve as an optimistic measure for better logging or runtime type checking. Making static annotations populate __orig_class__ is super out of scope and would be prohibitively expensive which means this construct will never be populated often enough for serious use.
When I wrote CheckedSet, CheckedList, and CheckedDict for EdgeDB, I couldn't depend on GenericAlias anyway since it sets __orig_class__ too late: you wouldn't be able to have runtime type checks during object instantiation which removes much of the point. To be maximally useful, Checked* types to make sense require that only parametrized types are instantiated. But we don't want this to become a prevalent idiom and to change Python code to Javaesque `l = list[str]()` everywhere.
4. We should probably arrange for list[int] == list[int] by defining GenericAlias.__eq__. (But list != list[int].)
5. Should we touch typing.py at all? Or just leave it be? Though the proposed silent deprecation will encourage people who *want* to do runtime introspection to ignore the changes, and then they will be in for a bad surprise 5 years hence.
I don't understand why the "runtime introspection" crowd would want to deliberately ignore the changes.
There's someone (Saul Shanabrook, who piped up in several recent threads here about PEP 585) who monkey-patches something in typing.py so that when a class method of a generic user-defined class is called, it is passed the parameterized class. Something like this:
... def create(cls): return cls
IIUC, Saul wants C[int].create() to return C[int], not just C.
In general, I think that when we remove typing.py, it should be downloadable from PyPI. My thinking was that deprecations shouldn't be visible on the Python side but on the type checker side.
OK; gotta run now. Thanks for running with this, I am very excited and grateful for this!