-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
TypeForm[T]: Spelling for regular types (int, str) & special forms (Union[int, str], Literal['foo'], etc) #9773
Comments
Why not do something like:
|
Why can't we make @hauntsaninja's workaround is useful, but it would be better to have a feature in the core type system for this. |
@hauntsaninja , using your workaround I am unable to access the passed parameter at runtime from inside the wrapper class. The following program: from __future__ import annotations
from typing import *
T = TypeVar('T')
class TypeAnnotation(Generic[T]):
@classmethod
def trycast(cls, value: object) -> Optional[T]:
Ts = get_args(cls)
print(f'get_args(cls): {Ts!r}')
return None
ta = TypeAnnotation[Union[int, str]]
print(f'get_args(ta): {get_args(ta)!r}')
result = ta.trycast('a_str') prints:
|
@JelleZijlstra commented:
I agree that having However it appears that there was a deliberate attempt in mypy 0.780 to narrow |
I believe the issue is that So if this feature is going to happen I think it should be a separate thing -- and for the static type system it probably shouldn't have any behavior, since such objects are only going to be useful for introspection at runtime. (And even then, how are you going to do the introspection? they all have types that are private objects in the typing module.) |
Well I wrote this module with various checks and add every new major py version to the tests to see that it keeps working: https://github.com/ltworf/typedload/blob/master/typedload/typechecks.py Luckily since py3.6 it has not happened that the typing objects change between minor upgrades of python. |
Makes sense.
Agreed.
The typing module itself provides a few methods that can be used for introspection. |
I could see a couple of possible spellings for the new concept:
Personally I'm now leaning toward (Let the bikeshedding begin. :) |
I like TypeForm. |
Okay I'll go with Next steps I expect are for me to familiarize myself with the mypy codebase again since I'm a bit rusty. Hard to believe it's been as long as since 2016 I put in the first version of TypedDict. Rumor is it that the semantic analyzer has undergone some extensive changes since then. |
Good. And yeah, a lot has changed. Once you have this working we should make a PEP out of it. |
Yep, will do this time around. :) |
Update: I redownloaded the high-level mypy codebase structure this afternoon to my brain. It appears there are now only 4 major passes of interest:
Next steps I expect are to trace everywhere that mypy is processing occurrences of |
Update: I have found/examined all mypy code related to processing the statement
Next steps I expect are to trace everywhere that mypy is processing occurrences of |
Update: I did trace everywhere that mypy is processing occurrences of Type[T], and more specifically uses of TypeType. There are a ton! In examining those uses it looks like the behavior of TypeForm when interacting with other type system features is not completely straightforward, and therefore not amenable to direct implementation. So I've decided to take a step back and start drafting the design for TypeForm in an actual PEP so that any design issues can be ironed out and commented on in advance. Once it's ready, I'll post a link for the new TypeForm PEP draft to here and probably also to typing-sig. |
Yeah, alas |
Aye. Variadic generics and type guards both have much wider applicability than TypeForm in my estimation. Python 3.10's upcoming alpha window from Feb 2021 thru April is coming up fast, and only so many PEPs can be focused on. Nevertheless I'll get the initial TypeForm PEP draft in place, even if it needs to be paused ("deferred"?) for a bit. |
Update: I've drafted an initial PEP for TypeForm. However I was thinking of waiting to post the TypeForm PEP for review (on typing-sig) until the commenting on PEP 646 (Variadic Generics) slows down and it becomes soft-approved, since that PEP is consuming a lot of reviewer time right now and is arguably higher priority. In the meantime I'm building out an example module (trycast) that plans to use TypeForm. |
Update: I'm still waiting on Variadic Generics (PEP 646) on typing-sig to be soft-approved before posting the TypeForm PEP draft, to conserve reviewer time. (In the meantime I'm continuing to work on trycast, a new library for recognizing JSON-like values that will benefit from TypeForm. Trycast is about 1-2 weeks away from a beta release.) |
I expect PEP 646 to be a complex topic to soft-approve, given the
complexity of implementation, so I recommend not blocking on that for too
long (though waiting a little while longer is fine).
|
From python/typeshed#11653, |
Same goes for |
@davidfstr I work on a compiler in Python that embeds constraints on the values in the IR into the Python type system. Some of these constraints are generic, representing nested constraints, for which we wrote a function that is similar to |
Next topic: Naming the concept of "the type of a type annotation object":
I also like
Comments? Support? Objections? |
I like AnnotationType more 😀 |
I don't like This specifically describes typing special forms and not annotations as a whole. I don't think TypeForm being "more jargony" is a large enough detraction to have a name that is actively more misleading about what it describes instead. |
Could you please move this discussion to the typing forum? This isn't a mypy-specific feature. It's a proposed change to the typing spec, so it deserves the visibility and broader input from the community. |
Sure. I'll make a new thread there in the next few days. |
In preparation for moving this discussion to the typing forum, I'm currently drafting a new (2024) version of the TypeForm PEP, incorporating various feedback. Hoping to be done later this week. |
The 2024 version of the TypeForm PEP is ready for review. Please see the thread in the Typing forum. |
Draft 2 of the TypeForm PEP (2024 edition) is ready for review. Please leave your comments either in that thread or as inline comments in the linked Google Doc. I especially solicit feedback from maintainers of runtime type checkers:
Please see §"Abstract", §"Motivation", and §"Common kinds of functions that would benefit from TypeForm" in the PEP to see how the TypeForm feature relates to specific functions in the library you maintain. |
attrs doesn't do anything with types except copying them around (type-checking logic is entirely via a Mypy plugin and/or dataclass transforms), so I don't have feedback. But as you mention in the PEP, my less-known child svcs would benefit! But it seems to be more of a trivial byproduct of a bigger thing that I have to admit don't fully understand. :) |
...heh. @beartype and
# Note that this PEP 484- and 647-compliant API is entirely the brain child of
# @asford (Alex Ford). If this breaks, redirect all ~~vengeance~~ enquiries to:
# https://github.com/asford
@overload
def is_bearable(
obj: object, hint: Type[T], *, conf: BeartypeConf = BEARTYPE_CONF_DEFAULT,
) -> TypeGuard[T]:
'''
:pep:`647`-compliant type guard conditionally narrowing the passed object to
the passed type hint *only* when this hint is actually a valid **type**
(i.e., subclass of the builtin :class:`type` superclass).
'''
@overload
def is_bearable(
obj: T, hint: Any, *, conf: BeartypeConf = BEARTYPE_CONF_DEFAULT,
) -> TypeGuard[T]:
'''
:pep:`647`-compliant fallback preserving (rather than narrowing) the type of
the passed object when this hint is *not* a valid type (e.g., the
:pep:`586`-compliant ``typing.Literal['totally', 'not', 'a', 'type']``,
which is clearly *not* a type).
''' This behaves itself under all Python versions – even Python 3.8 and 3.9, which lack # Portably import the PEP 647-compliant "typing.TypeGuard" type hint factory
# first introduced by Python >= 3.10, regardless of the current version of
# Python and regardless of whether this submodule is currently being subject to
# static type-checking or not. Praise be to MIT ML guru and stunning Hypothesis
# maintainer @rsokl (Ryan Soklaski) for this brilliant circumvention. \o/
#
# Usage of this factory is a high priority. Hinting the return of the
# is_bearable() tester with a type guard created by this factory effectively
# coerces that tester in an arbitrarily complete type narrower and thus type
# parser at static analysis time, substantially reducing complaints from static
# type-checkers in end user code deferring to that tester.
#
# If this submodule is currently being statically type-checked (e.g., mypy),
# intentionally import from the third-party "typing_extensions" module rather
# than the standard "typing" module. Why? Because doing so eliminates Python
# version complaints from static type-checkers (e.g., mypy, pyright). Static
# type-checkers could care less whether "typing_extensions" is actually
# installed or not; they only care that "typing_extensions" unconditionally
# defines this type factory across all Python versions, whereas "typing" only
# conditionally defines this type factory under Python >= 3.10. *facepalm*
if TYPE_CHECKING:
from typing_extensions import TypeGuard as TypeGuard
# Else, this submodule is currently being imported at runtime by Python. In this
# case, dynamically import this factory from whichever of the standard "typing"
# module *OR* the third-party "typing_extensions" module declares this factory,
# falling back to the builtin "bool" type if none do.
else:
TypeGuard = import_typing_attr_or_fallback(
'TypeGuard', TypeHintTypeFactory(bool)) I only vaguely understand what's happening there. If I understand correctly, acceptance of this PEP would enable @beartype to (A) dramatically simplify the above logic (e.g., by eliminating the need for After the release of Python 3.13.0, @beartype and all things like @beartype should now (A) globally replace all reference to def is_bearable(
obj: object, hint: TypeForm[T], *, conf: BeartypeConf = BEARTYPE_CONF_DEFAULT,
) -> TypeIs[T]: David Foster be praised! I rejoice at this. The @beartype codebase will once again become readable. Well... more readable. Also, users are now weeping tears of joy at this. Type narrowing will start doing something useful for once. Yet questions remain. The Demon Is In the Nomenclature: Name Haters Gonna HateThe 100-pound emaciated gorilla in the room is actually your own open issue, @davidfstr:
...heh. My answer is: "It's really not." I have no capitalist skin in this game. I barely know what a Typeform is. Yet, googling "python typeform" trivially yields nearly half-a-million hits. Googling "typeform" itself yields an astonishing 24 million hits – none of which have anything to do with typing systems and everything to do with TypeForm, the wildly successful tech startup I only marginally understand. Their search engine optimization (SEO) would probably frown and get a crinkled forehead if we trampled all over their heavily monetized brand space. Out of sheer courtesy to Typeform, Typeform clients, and my rapidly shrinking 401k plan, ...heh it's probably best that the CPython standard library not trample American capitalism. Leave that to the evening news. Even if Typeform wasn't a thing, Maybe I should. Now I will. Great. Thanks a lot, @davidfstr. My cluttered mind now has even more material baggage to lug. Oh, I Know. I Know! I've Got It. You're Just Gonna Love It. It's...
...heh. Who didn't see that one coming, huh? Seriously.
Likewise, let's consider globally replacing all usages of the corresponding term "form" throughout the PEP with "hint": e.g., # Instead of this, which makes my cross eyes squint even more than normal...
def isassignable[T](value: object, form: TypeForm[T]) -> TypeIs[T]: ...
# Let's do this! My wan and pale facial cheeks are now smiling.
def isassignable[T](value: object, hint: TypeHint[T]) -> TypeIs[T]: ... Assignability Raisers and Sorters: So Those Are Things Too Now, Huh?So. It comes to this. In the parlance of this PEP, the aforementioned
In the same way that >>> from beartype.door import is_subhint
# Test simple subclass relations.
>>> is_subhint(bool, int)
True
>>> is_subhint(int, int)
True
>>> is_subhint(str, int)
False
# Test less simple type hint relations.
>>> from typing import Any
>>> is_subhint(list, Any)
True
# Test brutally hurtful type hint relations that make me squint. My eyes!
>>> from collections.abc import Callable, Sequence
>>> is_subhint(Callable[[], list], Callable[..., Sequence[Any]])
True
>>> is_subhint(Callable[[], list], Callable[..., Sequence[int]])
False Is a partial ordering over the set of all types actually useful, though? I mean, sure. It's cool. We get that. Everything's cool if you squint enough at it. But does anyone care? ...heh. Yeah. It turns out a partial ordering over the set of all types unlocks the keys to the Kingdom of QA – including efficient runtime multiple dispatch in In the case of from beartype.door import die_if_unbearable
# Define something heinous dynamically. Static type-checkers no longer have any idea what's happening.
eval('muh_list = ["kk", "cray-cray", "hey, hey", "wut is going on with this list!?"])
# Beartype informs static type-checkers of that the type of "muh_list" is "list[str]".
die_if_unbearable(muh_list, list[str])
# Static type-checkers be like: "Uhh... I... I guess, bro. I guess. Seems wack. But you do you."
print(''.join(for muh_item in muh_list)) # <-- totally fine! accept this madness, static type-checker "TypeForm" Values Section: Not Sure What's Going On Here, But Now SquintingThe "TypeForm" Values section makes me squint. From @beartype's general-purpose broad-minded laissez faire "anything goes" perspective, anything that is a type hint should ideally be a This includes type hints that are only contextually valid in various syntactic and/or semantic contexts – like The PEP currently rejects these sorts of type hints as "annotated expressions" – which is itself really weird, because we already have annotated type hints that are technically Python expressions and thus "annotated expressions": The problem with rejecting some but not all type hints is:
Stringified TypeForms Section: NO GODS WHY NOOOOOOOOOOOOOOOOOOOOO
...heh. So. It comes to this. You're trying to commit @leycec to a sanitarium. The truth is now revealed. Please. Let's all be clear on this:
Static type-checkers don't care about stringified type hints, of course. But static type-checkers also hallucinate. By definition, their opinions are already insane. Runtime type-checkers, however, basically cannot cope with stringified type hints – like, any stringified type hints. In the general case, doing so requires non-portable call stack inspection. It's slow. It's fragile. It's non-portable. It basically never works right. Even when it works "right," it never works the way users expect. Sure. @beartype copes with stringified type hints – mostly. But @beartype is also insane. @beartype has already squandered years of sweaty blood, smelly sweat, unpaid man hours, and precious life force attempting to support insane shenanigans like PEP 563 (i.e., In 2024, with the benefit of hindsight and safety goggles, let us all quietly admit that stringified type hints were a shambolic zombie plague that should have never happened. We certainly shouldn't be expanding the size and scope of stringified type hints. We should be deprecating, obsoleting, and slowly backing away from stringified type hints with our arms placatingly raised up in the air as we repeatedly say: "We're sorry! We're sorry for what we did to you, Pydantic and @beartype and There's no demonstrable reason whatsoever to permit useless insanity like
@leycec: He Is Now Tired and Must Now Collapse onto a Bed Full of Cats |
LGTM. Excited about this! |
Yeah, just today I ran into a problem that would probably be solved with this. And it wasn't even related to |
A broad +1 to all of @leycec's points.
|
Name
Heh. True. "Typeform" is much better known as a service for online surveys. :) So, definitely a +1 that there's almost certainly a better name then "TypeForm". However choosing such a name depends Values
I've debated whether the TypeForm concept should cover all runtime type annotation objects (i.e. what the typing specification calls "annotation expressions") or only those objects which spell a "type" (i.e. what the typing specification calls "type expressions"). Allowing TypeForm[] to match non-types (like InitVar[], Final[], Self, etc) doesn't make sense when trying to combine TypeForm[] with TypeIs[] or TypeGuard[] in a function definition, one of the key capabilities I want to enable. I discuss this further in §"Rejected Ideas > Accept arbitrary annotation expressions". Consider the following code:
What should a static type checker infer for the Right now the PEP takes the stance that passing a non-type of Final[] where a TypeForm[] is expected is an error. So Surprisingly, I see that is_bearable (an implementation of isassignable from beartype) can return True in the above scenario...
Appendix: More adventures in beartypeHappily I see
And
But
Appendix: Similar adventures in trycastIn the trycast library,
(Heh. I especially need to fix that last error message to be something sensible.) Stringified TypeForms
Agreed that stringified TypeForms - where the entire type is a string, not just some interior forward references - are very difficult to work with at runtime. I allude to this in §"How to Teach This", but I thought I had used stronger language than what I now see: 😉
(Note to self: Increase emphasis in the PEP RE how difficult it is to work with stringified type annotations at runtime.) The current PEP draft defaults to allowing stringified TypeForms since static type checkers already expect & handle them robustly in locations where a type expression can appear. But - upon further thought - anything that can fit into a TypeForm must be capable of being well-supported both by static and runtime type checkers in order to spell an implementable function definition. So I'm inclined to agree that TypeForms probably shouldn't allow stringified annotations since they're basically impossible to work with robustly at runtime. Edit: I changed my mind RE not allowing stringified annotations to be matched, to prioritize aligning with matching all "type expressions" (which include them). |
I have a possibly-controversial suggestion (that I don't feel too strongly about right now), which is that this isn't defined behaviour. For example, if I fill in the type hint explicitly with pyright (which is what I have installed at the moment), then we get the perfectly meaningless: from typing import Final, TypeGuard
def foo(x) -> TypeGuard[Final[int]]:
pass
x = 1
if foo(x):
reveal_type(x) # Type of `x` is `Final`. To expand on this, the proposed |
I would argue that the types allowed by TypeForm should exactly match the definition of either "type expression" or "annotation expression" in the spec (https://typing.readthedocs.io/en/latest/spec/annotations.html#type-and-annotation-expressions). This reduces the number of concepts and makes the overall system simpler. Possibly we should add both, which suggests obvious names for the new special forms: I don't think it is practical to disallow stringified annotations. Consider a type alias from a third party library that is defined as There will always be some types that are hard for a runtime type checker to check. For example, To @patrick-kidger's example, pyright correctly shows an error on the |
To me, it feels like we should tend towards being as loose as possible with what is permitted as a For |
ValuesSeveral folks have recommended not bifurcating the existing concepts of "annotation expressions" and "type expressions" to a further third subset, and to instead just pick one of the first two. Since the main utility of TypeForm[] is using it in combination with TypeIs[] + TypeGuard[], and because a "type expression" is what those forms accept, I'm inclined to round the concept of TypeForm to exactly match a "type expression". NameWith the above meaning defined for the concept, I'm looking at renaming TypeForm[] to TypeExpression[]. There may be a desire to define a separate concept that aligns with "annotation expressions", perhaps called AnnotationExpression[], but I don't think it's valuable to define in this PEP. I don't see any benefits in being able to spell Stringified TypeFormsBy rounding the concept of TypeForm[] to exactly match a "type expression", that would imply that stringified annotations like Perhaps it would be sufficient in §"How to Teach This":
New idea: Matching TypeExpressions[] with an ABC?@erictraut has expressed concern that it would be difficult for a static type checker like pyright to match TypeExpressions[]s in locations that would normally accept only a regular value expression. 1 Static type checkers already have to deal with recognizing ABCs, so I wonder if defining a TypeExpression[] as an ABC would make it easier for a static type checker to recognize... A quick proof of concept: >>> from abc import ABC
>>> from typing import *
>>> import typing
>>>
>>> class TypeExpression(ABC):
... pass
>>>
>>> type(str)
<class 'type'>
>>> TypeExpression.register(type)
>>>
>>> type(Union[int, str])
<class 'typing._UnionGenericAlias'>
>>> TypeExpression.register(typing._UnionGenericAlias)
>>>
>>> isinstance(str, TypeExpression)
True
>>> isinstance(Union[int, str], TypeExpression)
True Footnotes |
We could also consider
I don't think that would work very well. The fact that |
@davidfstr I think this discussion should be happening on discourse as it affects more than just mypy. As for the concern Eric brought up, that was (part of) the reason for what I mentioned with |
Agreed. I have requested that folks respond on the discourse thread, but most responses have actually happened here so far. I have been giving counter-responses in the same venue that I received responses, which so far has been mostly here.
I'm not sure how
is related to
Yes, however I've already mentioned that: I’m not sure that its desirable (or even possible) to write out the exact constraints on what kinds of forms a particular function will accept. (And I think that’s OK.) I also responded further later in the thread. |
Draft 3 of the |
(An earlier version of this post used
TypeAnnotation
rather thanTypeForm
as the initially proposed spelling for the concept described here)Feature
A new special form
TypeForm[T]
which is conceptually similar toType[T]
but is inhabited by not only regular types likeint
andstr
, but also by anything "typelike" that can be used in the position of a type annotation at runtime, including special forms likeUnion[int, str]
,Literal['foo']
,List[int]
,MyTypedDict
, etc.Pitch
Being able to represent something like
TypeForm[T]
enables writing type signatures for new kinds of functions that can operate on arbitrary type annotation objects at runtime. For example:Several people have indicated interest in a way to spell this concept:
typing.Type
anymore #8992typing.Type
anymore #8992 (comment)For a more in-depth motivational example showing how I can use something like TypeForm[T] to greatly simplify parsing JSON objects received by Python web applications, see my recent thread on typing-sig:
If there is interest from the core mypy developers, I'm willing to do the related specification and implementation work in mypy.
The text was updated successfully, but these errors were encountered: