You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Merge 'types.UnionType' with typing._UnionGenericAlias, not typing.Union #137065
I think #105499 was a mistake. types.UnionType was intentionally named so to avoid confusion with typing.Union (see #88895). types.UnionType should not be subscriptable because it is not generic type (and if it was a generic type, subscription would have different semantic than for typing.Union). types.UnionType corresponds to typing._UnionGenericAlias, not typing.Union (see #89581 (comment)).
So we should restore typing.Union, make typing._UnionGenericAlias an alias of types.UnionType, and make types.UnionType non-subscriptable again.
#137069 restores typing.Union, subscribing which returns now a types.UnionType instance. types.UnionType is no longer subscripable. typing._UnionGenericAlias was left as it was -- unused and deprecated.
A private function is used to create types.UnionType instances. This is a temporary solution. The public ways are int | str and typing.Union[int, str]. We may add other public way in future (make types.UnionType callable or add a class method), but this needs further investigation.
I don't think there was a mistake and I don't see a case for changing the behavior yet again in the RC phase. What concrete problems does the current behavior cause?
types.UnionType should not be subscriptable, because it is not a generic type. There is a general rule to only use __class_getitem__ for parametrization of generic classes. It was its purpose.
typing.Union should not be a class. Initially the typing types were implemented as classes, but this caused many issues, so they all were reimplemented as non-classes.
I am sorry that I noticed this change so late, but it will be much more difficult to unmerge types.UnionType and typing.Union after release.
I feel this mistake here was that types.UnionType was added as a separate public type in 3.10. Things should have been implemented the way they are now in 3.14 in the first place, so introspection code doesn't always have to deal with two kinds of unions. Fortunately, we were able to make the change now with a minimum of compatibility implications.
Reacted by Kirill Podoprigora, Dmitriy, chiri and Anton Agestam
I think we should leave it merged. From the start, treating these two things as distinct was a mistake.
Users working with type hint introspection should support both types when they are semantically equivalent. In my opinion, int | str and Union[int, str] should have the same type in runtime.
Distinguishing between them creates unnecessary pain for libraries like adaptix/pydantic/etc.
Reacted by Thomas M Kehrenberg, chiri and Anton Agestam
That's indeed ambiguous, but given that rc3 went out without the revert, I assume it meant keeping the status quo in the 3.14 branch, that is with the merger in place.
I'd like to start with saying I'm fine with the status quo. A use case that was supported before and not anymore in 3.14 though is for tools to detect the type of union annotation the user was using, e.g., to replicate that in their docs. Discovered while doing tox-dev/sphinx-autodoc-typehints#569.
I'd like to start with saying I'm fine with the status quo. A use case that was supported before and not anymore in 3.14 though is for tools to detect the type of union annotation the user was using, e.g., to replicate that in their docs. Discovered while doing tox-dev/sphinx-autodoc-typehints#569.
The canonical way to check for unions is by using the following:
# On Python < 3.14:a=int|strb=typing.Union[int, str]
defis_union(obj):
origin=get_origin(obj)
returnisinstance(obj, types.UnionType) originistyping.Union# Extra safety, although shouldn't be necessary as `typing_extensions.Union` is `typing.Union` as of today:returnisinstance(obj, types.UnionType) originistyping.Unionororiginistyping_extensions.Unionis_union(a) # Trueis_union(b) # True# On Python >= 3.14:defis_union(obj):
origin=get_origin(obj)
returnisinstance(obj, typing.Union)
This implementation actually didn't have to be updated for 3.14, it was forward compatible to begin with. It also avoids relying on private typing names, as it is currently in tox-dev/sphinx-autodoc-typehints#569.
I think the point is that you can no longer distinguish between unions created through A | B and those created through Union[A, B]. That is true, but I'm not sure it should be possible. They're meant to do the same thing, just as it's not possible to distinguish between an int created with 0x10 and one created with 16.
Yeah, that was my point. With https://github.com/tox-dev/sphinx-autodoc-typehints before we could generate Union[A, B] in the documentation if hte user used the Union form, and A | B if they used the bar operator. Now there's no way to detect which variant they used so we're forced to pick one (or let the user explicitly say for all which they want).
I don't have the background to participate in the discussion about the design principles linked to this issue, but I figured I could share my 2c as an uninitiated user.
For the type recursion use case (see also #89581), the biggest pain for me is that typing allows you to deconstruct a type with get_origin/get_args, but doesn't offer a way to reconstruct a type with those values as parameters:
origin, args=get_origin(A), get_args(A)
# Maybe you want to do some manipulation here:args=f(args)
B=origin[args] # Intuitively what I would expect, given documentation of get_origin/get_argsB=typing.construct_generic(origin, args) # Less pretty, but would also be workableB=Union[args] iforiginisUnionTypeelseorigin[args] # Current workaround
It's not clear to me from the docs if this is the only special case you can expect, either. As such, I would be hesitant to introduce the workaround above, because I'm not sure if this can break without warning, or if I'm missing more edge cases. And trying to dig up all the details of the implementation is not worth the time investment for what I'm trying to do.
So to me, #105499 looks like an improvement if it means that get_origin(A)[get_args(A)] == A for any reasonably constructed type A.
Edit: Just noticing now that latest doc do already state that A | B is equivalent to Union[A, B], so I guess this is already resolved?
B = origin[args] # Intuitively what I would expect, given documentation of get_origin/get_args
There are a number of cases where this isn't enough unfortunately. In almost all cases, we can use public/documented APIs to reconstruct generic aliases, except for typing._GenericAlias instances where it is required to use the copy_with() method.
Activity
pythongh-137065: Unmerge types.UnionType and typing.Union
pythongh-137065: Unmerge types.UnionType and typing.Union
serhiy-storchaka commentedon Jul 24, 2025
#137069 restores
typing.Union, subscribing which returns now atypes.UnionTypeinstance.types.UnionTypeis no longer subscripable.typing._UnionGenericAliaswas left as it was -- unused and deprecated.A private function is used to create
types.UnionTypeinstances. This is a temporary solution. The public ways areint | strandtyping.Union[int, str]. We may add other public way in future (maketypes.UnionTypecallable or add a class method), but this needs further investigation.JelleZijlstra commentedon Jul 24, 2025
I don't think there was a mistake and I don't see a case for changing the behavior yet again in the RC phase. What concrete problems does the current behavior cause?
serhiy-storchaka commentedon Jul 24, 2025
types.UnionTypeshould not be subscriptable, because it is not a generic type. There is a general rule to only use__class_getitem__for parametrization of generic classes. It was its purpose.typing.Unionshould not be a class. Initially thetypingtypes were implemented as classes, but this caused many issues, so they all were reimplemented as non-classes.I am sorry that I noticed this change so late, but it will be much more difficult to unmerge
types.UnionTypeandtyping.Unionafter release.JelleZijlstra commentedon Jul 24, 2025
Those are not concrete problems.
I feel this mistake here was that
types.UnionTypewas added as a separate public type in 3.10. Things should have been implemented the way they are now in 3.14 in the first place, so introspection code doesn't always have to deal with two kinds of unions. Fortunately, we were able to make the change now with a minimum of compatibility implications.serhiy-storchaka commentedon Jul 24, 2025
typing._UnionGenericAliasandtyping.Unionare two different things.types.UnionTypeis a builtin implementation oftyping._UnionGenericAlias.Please, let fix this mistake while it can be done for small cost.
Eclips4 commentedon Jul 25, 2025
I think we should leave it merged. From the start, treating these two things as distinct was a mistake.
Users working with type hint introspection should support both types when they are semantically equivalent. In my opinion,
int | strandUnion[int, str]should have the same type in runtime.Distinguishing between them creates unnecessary pain for libraries like adaptix/pydantic/etc.
3 remaining items
pythongh-137065: Revert "pythongh-105499: Merge typing.Union and type…
pythongh-137065: Revert "pythongh-105499: Merge typing.Union and type…
hugovk commentedon Sep 18, 2025
SC decided on the status quo.
serhiy-storchaka commentedon Sep 18, 2025
Just in case, I want to clarify if I understood you correctly. Do you mean restoring the status quo?
JelleZijlstra commentedon Sep 18, 2025
That's indeed ambiguous, but given that rc3 went out without the revert, I assume it meant keeping the status quo in the 3.14 branch, that is with the merger in place.
hugovk commentedon Sep 18, 2025
Yes, sorry for the ambiguity; I meant without the revert. The SC will post their decision later.
gaborbernat commentedon Oct 8, 2025
I'd like to start with saying I'm fine with the status quo. A use case that was supported before and not anymore in 3.14 though is for tools to detect the type of union annotation the user was using, e.g., to replicate that in their docs. Discovered while doing tox-dev/sphinx-autodoc-typehints#569.
hugovk commentedon Oct 9, 2025
For reference, it's here: https://discuss.python.org/t/types-uniontype-was-merged-with-wrong-class-not-even-a-class/102275/30
Viicos commentedon Oct 9, 2025
Edit: outdated, see below
The canonical way to check for unions is by using the following:
This implementation actually didn't have to be updated for 3.14, it was forward compatible to begin with. It also avoids relying on private typing names, as it is currently in tox-dev/sphinx-autodoc-typehints#569.
You may be interested in https://github.com/pydantic/typing-inspection, which will save you from typing introspection headaches.
JelleZijlstra commentedon Oct 9, 2025
I think the point is that you can no longer distinguish between unions created through
A | Band those created throughUnion[A, B]. That is true, but I'm not sure it should be possible. They're meant to do the same thing, just as it's not possible to distinguish between an int created with0x10and one created with16.gaborbernat commentedon Oct 9, 2025
Yeah, that was my point. With https://github.com/tox-dev/sphinx-autodoc-typehints before we could generate
Union[A, B]in the documentation if hte user used theUnionform, andA | Bif they used the bar operator. Now there's no way to detect which variant they used so we're forced to pick one (or let the user explicitly say for all which they want).Viicos commentedon Oct 9, 2025
Oh sorry, read to fast!
scranen commentedon Aug 18, 2026
I don't have the background to participate in the discussion about the design principles linked to this issue, but I figured I could share my 2c as an uninitiated user.
For the type recursion use case (see also #89581), the biggest pain for me is that
typingallows you to deconstruct a type withget_origin/get_args, but doesn't offer a way to reconstruct a type with those values as parameters:It's not clear to me from the docs if this is the only special case you can expect, either. As such, I would be hesitant to introduce the workaround above, because I'm not sure if this can break without warning, or if I'm missing more edge cases. And trying to dig up all the details of the implementation is not worth the time investment for what I'm trying to do.
So to me, #105499 looks like an improvement if it means that
get_origin(A)[get_args(A)] == Afor any reasonably constructed typeA.Edit: Just noticing now that latest doc do already state that
A | Bis equivalent toUnion[A, B], so I guess this is already resolved?Viicos commentedon Aug 18, 2026
There are a number of cases where this isn't enough unfortunately. In almost all cases, we can use public/documented APIs to reconstruct generic aliases, except for
typing._GenericAliasinstances where it is required to use thecopy_with()method.I have a work in progress implementation for this in
typing-inspection, a library for manipulating types, and the logic can be seen here.