I'm a +1 on using Annotated in this manner. Guido mentioned that it was
intended for only third-parties though. I'd like to know more about why
this isn't a good pattern for use by Python libraries. 

On Sun, 2021-02-14 at 16:29 +0100, Adrian Freund wrote:
> 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
> > 
> > 
> > _______________________________________________
> > 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-d
> > e...@python.org/message/KFKTZ6L5MI6S7KZY4W6PGZZWRR2PQTQF/
> > 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/UBNPEPAB2NBHY6KGDHF5SXMDVDPZH77U/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to