Skip to content

GenericAlias type equality checks with builtin generics are failing  #97543

Closed as not planned

Description

@art049

Bug report

This works as expected and as presented in the documentation

from typing import  GenericAlias

assert GenericAlias(tuple, (int, str)) == tuple[int, str]

However, using the legacy typing interface, the equality check fails

from typing import  GenericAlias, Tuple

assert GenericAlias(Tuple, (int, str)) == Tuple[int, str]
#> AssertionError

This also fails (and shouldn't work anyway)

from typing import  GenericAlias, Tuple

assert GenericAlias(Tuple, (int, str)) == tuple[int, str]
#> AssertionError

Your environment

  • CPython versions tested on: Python 3.9.12
  • Operating system and architecture: Manjaro 5.15 x86_64

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    on Sep 25, 2022
  2. Fidget-Spinner commented on Sep 25, 2022

    @Fidget-Spinner
    Member

    Thank you for the report. IMO this isn't a bug. The primary reason is that these aren't even the same objects. It's also why this evaluates to False:

    >>> from typing import Tuple
    >>> Tuple[int, str] == tuple[int, str]
    False
    

    Could you please explain your use case and why this would be useful to you?

  3. art049 commented on Sep 25, 2022

    @art049
    ContributorAuthor

    Hello, yes the second one should not work at all my bad (I did an edit).
    I'm substituting types at runtime so for example, if someone defines Dict[str,A], it'll end up being Dict[str,ImprovedA].

    I faced the issue while writing tests for this substitution function. I ended up implementing the equality check recursively like this:

    from typing import get_args, get_origin
    
    def are_generics_equal(g1, g2) -> bool:
        if g1 == g2:
            return True
        origin_g1 = get_origin(g1)
        origin_g2 = get_origin(g2)
        if origin_g1 is None or origin_g2 is None or origin_g1 != origin_g2:
            return False
        args_g1 = get_args(g1)
        args_g2 = get_args(g2)
        if len(args_g1) != len(args_g2):
            return False
        return all(are_generics_equal(a1, a2) for a1, a2 in zip(args_g1, args_g2))

    So in the end it's not critical but I expected Tuple[int,str] == GenericAlias(Tuple,(int,str)) to be true.

  4. Fidget-Spinner commented on Sep 25, 2022

    @Fidget-Spinner
    Member

    Tuple[int,str] == GenericAlias(Tuple,(int,str)) wasn't really considered when this was implemented I think. When the GenericAlias constructor was exposed, I'm not sure the implementers expected people to dynamically create types like this.

    Thoughts @serhiy-storchaka @gvanrossum or @JelleZijlstra ? Should we allow this? I'm on the fence.

  5. sobolevn commented on Sep 25, 2022

    @sobolevn
    Member

    There are two different GenericAlias types: from types and from typing

    >>> type(Tuple[int, str])
    <class 'typing._GenericAlias'>
    
    >>> type(tuple[int, str])
    <class 'types.GenericAlias'>

    So, when you compare types.GenericAlias to typing._GenericAlias they are not equal.
    It is 100% not a bug.

    But, if you are asking for a new feature so Tuple[int, str] == tuple[int, str] is True, that's a completely different story.

  6. Fidget-Spinner commented on Sep 25, 2022

    @Fidget-Spinner
    Member

    But, if you are asking for a new feature ..., that's a completely different story.

    Yeah I interpreted this is a feature request. Specifically for the case where both __origin__ and __args__ for a typing._GenericAlias and types.GenericAlias are equal, should they be equal?

    I think we probably can't change things due to backwards compatbility though.

  7. added
    type-featureA feature request or enhancement
    and removed
    type-bugAn unexpected behavior, bug, or error
    on Sep 25, 2022
  8. art049 commented on Sep 25, 2022

    @art049
    ContributorAuthor

    There are two different GenericAlias types: from types and from typing

    Ah, thanks! I didn't notice that those were two different GenericAlias types.

    Yeah I interpreted this is a feature request. Specifically for the case where both origin and args for a typing._GenericAlias and types.GenericAlias are equal, should they be equal?

    Yes this is the feature request then 😅

  9. sobolevn commented on Sep 25, 2022

    @sobolevn
    Member

    I not sure that tuple[int, str] should be equal to Tuple[int, str], right now it is:

    >>> from typing import Tuple
    >>> tuple[int, str] == Tuple[int, str]
    False

    The code around typing is changing really fast and it quite hard to maintain libraries that work with runtime annotations. We should not complicate it even more for no good reason.

    But, changing that is quite easy:

    Снимок экрана 2022-09-25 в 18 24 41

    You can at least write your custom comparator for now 😉

  10. hauntsaninja commented on Sep 25, 2022

    @hauntsaninja
    Contributor

    +1 to not doing this feature request and in particular to this statement:

    The code around typing is changing really fast ... We should not complicate it even more for no good reason.

  11. added
    pendingThe issue will be closed if no feedback is provided
    on Sep 25, 2022
  12. hmc-cs-mdrissi commented on Sep 26, 2022

    @hmc-cs-mdrissi
    Contributor

    I don't have much of view on Tuple[int,str] == GenericAlias(Tuple,(int,str)) as I haven't interacted much with creating types like this and mostly treat GenericAlias as internal thing.

    Tuple[int, str] == tuple[int, str] though I have encountered and seen runtime type using libraries either fail with a bug when situations like this happen or have to implement workarounds. Using one popular library, beam is a library for writing map reduce style transforms. Toy example,

    import apache_beam as beam
    
    def add_parity(n: int) -> tuple[bool, int]:
        return (n % 2 == 0, n)
    
    with beam.Pipeline() as p:
        numbers = p | "Create" >> beam.Create([1, 2, 3, 4, 5])
        numbers = numbers | beam.Map(add_parity)
        _ = numbers | beam.GroupByKey() | beam.Map(print)

    This code partly crashes because of tuple vs Tuple distinction. beam checks that type hints of transforms are consistent with each other and beam.GroupByKey uses Tuple while add_parity used tuple. If I replace tuple -> Tuple code works. One tricky point here is while tuple is used in user code, GroupByKey is library code that uses Tuple. So very easy place for mismatch that's not easy to spot.

    At same time while tuple[int, str] == Tuple[int, str] as true can help, there are more differences in two that I think would cause similar things to fail anyway and probably should file a bug report on beam's side. Making them fully match up feels like it may be a series of weird differences that become more and more ambiguous what's expected to be same vs internal detail. It may be easier/more helpful if the typing utilities that libraries typically want instead had a function. Main function beam wants here is,

    def typing_compatible(type1, type2):
      # Roughly issubclass(type1, type2) but also needs to allow things like Any is always compatible,
      # and accept stuff like list[str] is compatible with list[T] where T is a type variable.
      ...

    and they implement there own version here.

    edit: Although one question here is whether typing_compatible is common enough in runtime type usage to be similar to get_origin/get_args and be a feature request or if it should lie in an external library (like typing_inspect).

  13. Fidget-Spinner commented on Sep 26, 2022

    @Fidget-Spinner
    Member

    Sorry. I'm closing this issue because the general consensus by the typing contributors here is that we shouldn't complicate the runtime aspect of typing. I also want to point out that this is backwards incompatible so even if we do want to do it thats not possible for the next 2 versions.

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

    pendingThe issue will be closed if no feedback is providedtopic-typingtype-featureA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions