Skip to content

Merge 'types.UnionType' with typing._UnionGenericAlias, not typing.Union #137065

Description

@serhiy-storchaka

Bug report

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.

Linked PRs

Activity

  1. added 2 commits that reference this issue on Jul 24, 2025
  2. serhiy-storchaka commented on Jul 24, 2025

    @serhiy-storchaka
    MemberAuthor

    #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.

  3. JelleZijlstra commented on Jul 24, 2025

    @JelleZijlstra
    Member

    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?

  4. serhiy-storchaka commented on Jul 24, 2025

    @serhiy-storchaka
    MemberAuthor
    1. 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.
    2. 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.

  5. JelleZijlstra commented on Jul 24, 2025

    @JelleZijlstra
    Member

    Those are not concrete problems.

    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.

  6. serhiy-storchaka commented on Jul 24, 2025

    @serhiy-storchaka
    MemberAuthor

    typing._UnionGenericAlias and typing.Union are two different things. types.UnionType is a builtin implementation of typing._UnionGenericAlias.

    Please, let fix this mistake while it can be done for small cost.

  7. Eclips4 commented on Jul 25, 2025

    @Eclips4
    Member

    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.

  8. 3 remaining items

  9. added 2 commits that reference this issue on Sep 16, 2025
  10. hugovk commented on Sep 18, 2025

    @hugovk
    Member

    SC decided on the status quo.

  11. serhiy-storchaka commented on Sep 18, 2025

    @serhiy-storchaka
    MemberAuthor

    Just in case, I want to clarify if I understood you correctly. Do you mean restoring the status quo?

  12. JelleZijlstra commented on Sep 18, 2025

    @JelleZijlstra
    Member

    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.

  13. hugovk commented on Sep 18, 2025

    @hugovk
    Member

    Yes, sorry for the ambiguity; I meant without the revert. The SC will post their decision later.

  14. gaborbernat commented on Oct 8, 2025

    @gaborbernat
    Contributor

    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.

  15. hugovk commented on Oct 9, 2025

    @hugovk
    Member

    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

  16. Viicos commented on Oct 9, 2025

    @Viicos
    Contributor
    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.

  17. JelleZijlstra commented on Oct 9, 2025

    @JelleZijlstra
    Member

    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.

  18. gaborbernat commented on Oct 9, 2025

    @gaborbernat
    Contributor

    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).

  19. Viicos commented on Oct 9, 2025

    @Viicos
    Contributor

    Oh sorry, read to fast!

  20. scranen commented on Aug 18, 2026

    @scranen

    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_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)] == 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?

  21. Viicos commented on Aug 18, 2026

    @Viicos
    Contributor

    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.

    I have a work in progress implementation for this in typing-inspection, a library for manipulating types, and the logic can be seen here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.14bugs and security fixes3.15bugs and security fixestopic-typingtype-bugAn unexpected behavior, bug, or error

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions