Repository navigation
Merge 'types.UnionType' with typing._UnionGenericAlias, not typing.Union #137065
Description
Activity
- addedtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error3.14bugs and security fixesbugs and security fixes3.15bugs and security fixesbugs and security fixes
on 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.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.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.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.Reacted by Kirill Podoprigora, Dmitriy, chiri and Anton Agestamtyping._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.
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.Reacted by Thomas M Kehrenberg, chiri and Anton Agestam3 remaining items
SC decided on the status quo.
Just in case, I want to clarify if I understood you correctly. Do you mean restoring the status quo?
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.
Yes, sorry for the ambiguity; I meant without the revert. The SC will post their decision later.
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 SC will post their decision later.
For reference, it's here: https://discuss.python.org/t/types-uniontype-was-merged-with-wrong-class-not-even-a-class/102275/30
Edit: outdated, see below
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 | str b = typing.Union[int, str] def is_union(obj): origin = get_origin(obj) return isinstance(obj, types.UnionType) origin is typing.Union # Extra safety, although shouldn't be necessary as `typing_extensions.Union` is `typing.Union` as of today: return isinstance(obj, types.UnionType) origin is typing.Union or origin is typing_extensions.Union is_union(a) # True is_union(b) # True # On Python >= 3.14: def is_union(obj): origin = get_origin(obj) return isinstance(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.
You may be interested in https://github.com/pydantic/typing-inspection, which will save you from typing introspection headaches.
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.Reacted by Ülgen SarıkavakYeah, 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).Oh sorry, read to fast!
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: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_args B = typing.construct_generic(origin, args) # Less pretty, but would also be workable B = Union[args] if origin is UnionType else origin[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)] == 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?B = origin[args]# Intuitively what I would expect, given documentation of get_origin/get_argsThere 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.
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsTodo
Bug report
I think #105499 was a mistake.
types.UnionTypewas intentionally named so to avoid confusion withtyping.Union(see #88895).types.UnionTypeshould not be subscriptable because it is not generic type (and if it was a generic type, subscription would have different semantic than fortyping.Union).types.UnionTypecorresponds totyping._UnionGenericAlias, nottyping.Union(see #89581 (comment)).So we should restore
typing.Union, maketyping._UnionGenericAliasan alias oftypes.UnionType, and maketypes.UnionTypenon-subscriptable again.Linked PRs