Here's another suggestion:

PEP 593 introduced the `Annotated` type annotation. This could be used to 
annotate a TypeGuard like this:

`def is_str_list(val: List[object]) -> Annotated[bool, TypeGuard(List[str])]`

Note that I used ( ) instead of [ ] for the TypeGuard, as it is no longer a 
type.

This should fulfill all four requirements, but is a lot more verbose and 
therefore also longer.
It would also be extensible for other annotations.

For the most extensible approach both `-> TypeGuard(...)`  and `-> 
Annotated[bool, TypeGuard(...)]` could be allowed, which would open the path 
for future non-type-annotations, which could be used regardless of whether the 
code is type-annotated.


--
Adrian

On February 14, 2021 2:20:14 PM GMT+01:00, Steven D'Aprano 
<st...@pearwood.info> wrote:
>On Sat, Feb 13, 2021 at 07:48:10PM -0000, Eric Traut wrote:
>
>> I think it's a reasonable criticism that it's not obvious that a 
>> function annotated with a return type of `TypeGuard[x]` should return 
>> a bool.
>[...]
>> As Guido said, it's something that a developer can easily 
>> look up if they are confused about what it means.
>
>Yes, developers can use Bing and Google :-)
>
>But it's not the fact that people have to look it up. It's the fact that 
>they need to know that this return annotation is not what it seems, but 
>a special magic value that needs to be looked up.
>
>That's my objection: we're overloading the return annotation to be 
>something other than the return annotation, but only for this one 
>special value. (So far.) If you don't already know that it is special, 
>you won't know that you need to look it up to learn that its special.
>
>
>> I'm open to alternative formulations that meet the following requirements:
>>
>> 1. It must be possible to express the type guard within the function 
>> signature. In other words, the implementation should not need to be 
>> present. This is important for compatibility with type stubs and to 
>> guarantee consistent behaviors between type checkers.
>
>When you say "implementation", do you mean the body of the function?
>
>Why is this a hard requirement? Stub files can contain function 
>bodies, usually `...` by convention, but alternatives are often useful, 
>such as docstrings, `raise NotImplementedError()` etc.
>
>https://mypy.readthedocs.io/en/stable/stubs.html
>
>I don't think that the need to support stub files implies that the type 
>guard must be in the function signature. Have I missed something?
>
>
>> 2. It must be possible to annotate the input parameter types _and_ the 
>> resulting (narrowed) type. It's not sufficient to annotate just one or 
>> the other.
>
>Naturally :-)
>
>That's the whole point of a type guard, I agree that this is a truly 
>hard requirement.
>
>
>> 3. It must be possible for a type checker to determine when narrowing 
>> can be applied and when it cannot. This implies the need for a bool 
>> response.
>
>Do you mean a bool return type? Sorry Eric, sometimes the terminology 
>you use is not familiar to me and I have to guess what you mean.
>
>
>> 4. It should not require changes to the grammar because that would 
>> prevent this from being adopted in most code bases for many years.
>
>Fair enough.
>
>
>> Mark, none of your suggestions meet these requirements.
>
>Mark's suggestion to use a variable annotation in the body meets 
>requirements 2, 3, and 4. As I state above, I don't think that 
>requirement 1 needs to be a genuinely hard requirement: stub files can 
>include function bodies.
>
>To be technically precise, stub functions **must** include function 
>bodies. It's just that by convention we use typically use `...` as the 
>body.
>
>
>> Gregory, one of your suggestions meets these requirements:
>> 
>> ```python
>> def is_str_list(val: Constrains[List[object]:List[str]) -> bool:
>>     ...
>> ```
>
>That still misleadingly tells the reader (or naive code analysis 
>software) that parameter val is of type
>
>    Contrains[List[object]:List[str]]
>
>whatever that object is, rather than what it *actually* is, namely 
>`List[object]`. I dislike code that misleads the reader.
>
>
>> As for choosing the name of the annotation
>[...]
>> `TypeGuard` is the term that is used in other languages to describe 
>> this notion, so it seems reasonable to me to adopt this term
>
>Okay, this reasoning makes sense to me. Whether spelled as a decorator 
>or an annotation, using TypeGuard is reasonable.
>
>
>> Steven, you said you'd like to explore a decorator-based formulation. 
>> Let's explore that. Here's what that it look like if we were to meet 
>> all of the above requirements.
>> 
>> ```python
>> @type_guard(List[str])
>> def is_str_list(val: List[object]) -> bool: ...
>> ```
>
>Okay.
>
>
>I note that this could be easily extended to support narrowing in the 
>negative case as well:
>
>    @type_guard(List[str], List[float])
>    def is_str_list(val: List[Union[str, float]]) -> bool: ...
>
>
>> The problem here, as I mention in the "rejected ideas" section of the 
>> PEP, is that even with postponed type evaluations (as described in PEP 
>> 563), the interpreter cannot postpone the evaluation of an expression 
>> if it's used as the argument to a decorator. That's because it's not 
>> being used as a type annotation in this context.
>
>Agreed.
>
>
>> So while Mark is 
>> correct to point out that there has been a mechanism available for 
>> forward references since PEP 484,
>
>That was actually me who pointed out the quoting mechanism for forward 
>references. (Unless Mark also did so.)
>
>
>> we've been trying to eliminate the 
>> use of quoted type expressions in favor of postponed evaluation. This 
>> would add a new case that can't be handled through postponed 
>> evaluation. Perhaps you still don't see that as a strong enough 
>> justification for rejecting the decorator-based formulation. I'm not 
>> entirely opposed to using a decorator here, but I think on balance 
>> that the `TypeGuard[x]` formulation is better. Once again, that's a 
>> subjective opinion.
>
>I understand the desire to minimize the use of quoted forward 
>references. But the cost to save two quote characters seems high: 
>changing an obvious and straight-forward return annotation to an 
>actively misleadingly special case. (Also, see below for the `Callable` 
>case.)
>
>I'm not convinced that forward references will be common. Did I miss 
>something, or are there no examples in the PEP that require a 
>forward-reference?
>
>
>    # Spam is not defined yet, so a forward reference is needed.
>
>    def is_list_of_spam(values:List[object]) -> TypeGuard[List[Spam]]:
>        return all(isinstance(x, Spam) for x in values)
>
>    # versus decorator
>
>    @type_guard('List[Spam]')
>    def is_list_of_spam(values:List[object]) -> bool:
>        return all(isinstance(x, Spam) for x in values)
>
>
>Of course, neither of these examples will actually work, because Spam 
>doesn't exist so you can't refer to it in the body. I don't get the 
>sense that this will require forward-references very often. At least not 
>often enough to justify obfuscating the return type.
>
>This obfuscation appears to have a critical consequence too. Please 
>correct me if I am wrong, but quoting your PEP:
>
>"""
>In all other respects, TypeGuard is a distinct type from bool. It is
>not a subtype of bool. Therefore, Callable[..., TypeGuard[int]] is not
>assignable to Callable[..., bool].
>"""
>
>If I am reading this correctly that implies that if I define these 
>functions:
>
>
>```
>def is_str_list(val: List[object]) -> TypeGuard[List[str]]:
>    return all(isinstance(x, str) for x in val)
>
>def my_filter(func:Callable[object, bool], values:List[object]) -> 
>List[object]:
>    return [x for x in values if func(x)]
>```
>
>the type checker would be expected to flag this as invalid:
>
>    my_filter(is_str_list, ['a', 'b'])
>
>
>If I have not misunderstood, surely this is a critical flaw with the 
>PEP?
>
>Eric, for what it's worth, I think that this will be an excellent 
>feature for type checkers, thank you for writing the PEP. It's just the 
>syntax gives me the willies:
>
>- special case handling of TypeGuard in a way that obfuscates the actual 
>  return type;
>
>- actively misleads the reader, and any naive code analysis tools that
>  don't know about type guards;
>
>- prevents user type guards from being seen as `Callable[..., bool]` 
>  even though they actually are.
>
>And the justification for eliminating decorators seems to be weak to me.
>
>However, I will give you one possible point in its favour: runtime 
>introspection of the annotations by typeguard-aware tools. If an 
>introspection tool is aware of the special meaning of `TypeGuard` in the 
>return annotation, then it is easy to introspect that value at runtime:
>
>    # untested...
>    T = func.__annotations__['return']
>    if T.startswith("TypeGuard"):
>        print("return type is bool")
>        print("narrows to type ...")  # TODO: extract from T
>    else:
>        print(f"return type is {T}")
>
>           
>With a decorator, we would need some other solution for runtime 
>introspection of the type guard. That's easy enough to solve, the 
>obvious solution is to just add the information to the function as an 
>attribute. But it is one additional complication.
>
>Still, I think this is a minor issue, especially compared to the 
>critical issue of `Callable[..., bool]`.
>
>I think that the runtime introspection issue will probably rule out 
>Mark's "variable annotation in the body" idea. So considering all the 
>factors as I see them, including familiarity to TypeScript devs:
>
>* Decorator: +1
>
>* Mark's variable annotation, if runtime introspection is neglected: +0.1
>
>* Gregory's idea to annotate the parameter itself: -0.1
>
>* The PEP's return value annotation: -0.2
>
>* Mark's variable annotation, if runtime introspection is required: -1
>
>
>-- 
>Steve
>_______________________________________________
>Python-Dev mailing list -- python-dev@python.org
>To unsubscribe send an email to python-dev-le...@python.org
>https://mail.python.org/mailman3/lists/python-dev.python.org/
>Message archived at 
>https://mail.python.org/archives/list/python-dev@python.org/message/OE6N443ASDFAH2KSBZ5BJX6UFNZKEAWM/
>Code of Conduct: http://python.org/psf/codeofconduct/
_______________________________________________
Python-Dev mailing list -- python-dev@python.org
To unsubscribe send an email to python-dev-le...@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at 
https://mail.python.org/archives/list/python-dev@python.org/message/KFKTZ6L5MI6S7KZY4W6PGZZWRR2PQTQF/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to