Skip to content

[3.14] annotationlib - Union '|' syntax and typing.Union[...] generate different forward references. #132805

Description

@DavidCEllis

Bug report

Bug description:

While looking into #125618 I ran into this case.

Using the union syntax gives a ForwardRef that can't be evaluated as the class that can be evaluated gets converted into its repr via ast.Constant. Using the typing.Union class however gives a proper union object where only the undefined value is a forwardref.

  • str | undefined -> ForwardRef("<class 'str'> | undefined")
  • Union[str, undefined] -> str | ForwardRef("undefined")

Example:

from annotationlib import get_annotations, Format
from typing import Union

class DifferentUnions:
    attrib: str | undefined
    other_attrib: Union[str, undefined]

different_unions = get_annotations(DifferentUnions, format=Format.FORWARDREF)

print(different_unions)

Formatted Output:

{
    'attrib': ForwardRef("<class 'str'> | undefined", is_class=True, owner=<class '__main__.DifferentUnions'>), 
    'other_attrib': str | ForwardRef('undefined', is_class=True, owner=<class '__main__.DifferentUnions'>)
}

One possible solution to this is to add a create_unions attribute to the _StringifierDict. If this is True then the __or__ and __ror__ methods should create types.UnionType instances instead of calling __make_new. This is False for Format.STRING in order to keep a | b in the reproduction in that case.

I already have a branch with this approach so I can make a PR.

Doing this will break the current test_nonexistent_attribute ForwardRef test as some | obj would evaluate to ForwardRef('some') | ForwardRef('obj') instead of ForwardRef('some | obj') but I think this is probably correct and the test should be changed.

CPython versions tested on:

CPython main branch

Operating systems tested on:

No response

Linked PRs

Activity

  1. JelleZijlstra commented on Apr 22, 2025

    @JelleZijlstra
    Member

    Good catch. I think this is a more general problem though, for example:

    >>> class X:
    ...     y: foo[str, undefined]
    ...     
    >>> get_annotations(X, format=Format.FORWARDREF)
    {'y': ForwardRef("foo[<class 'str'>, undefined]", is_class=True, owner=<class '__main__.X'>)}
    

    A more general approach to fix this would be to change the case in __convert_to_ast where we currently return ast.Constant(). Instead, we could pick a unique name, emit an ast.Name referring to that name, and make the ForwardRef remember a dictionary mapping such names to the original value. So in this case, we'd get something like ForwardRef("foo[$name1, undefined]", known_values={"$name1": str}). I can look into implementing that.

  2. DavidCEllis commented on Apr 22, 2025

    @DavidCEllis
    ContributorAuthor

    Ah, yes you're right. Sounds similar to what dataclasses needs to do to handle the values it needs for default values/types in exec. I'd only looked at exists[str, undefined] and not the case where the base object was undefined too.

  3. added
    stdlibStandard Library Python modules in the Lib/ directory
    3.14bugs and security fixes
    and removed on Apr 24, 2025
  4. added a commit that references this issue on May 4, 2025
  5. added a commit that references this issue on Jul 12, 2025
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 fixesstdlibStandard Library Python modules in the Lib/ directorytopic-typingtype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions