Skip to content

Conversation

KotlinIsland
Copy link
Contributor

@KotlinIsland KotlinIsland commented Sep 8, 2025

UnionType.__or__/__ror__

from types import UnionType
from typing import _SpecialForm

def f(u: UnionType, s: _SpecialForm):
    x = u | s
    reveal_type(x)  # UnionType, is actually _SpecialForm (is actually _UnionGenericAlias)
f(int | str, Literal[1])

this is rather unfortunate due to NotImplemented semantics, which are underspecified at this time, very relevant:

to work around this we add the known case _SpecialForm to the return type

and

x = int | int | int
reveal_type(x) # UnionType, is actually type[int]

to work around this we add the special case type to the return type, in the case of "unions are invalid as return types", then i would suggest UnionType | Any

UnionType.__getitem__

T = TypeVar("T")
A = int | list[T]

A[int]

it has __getitem__, the design was inspired by _SpecialForm.__getitem__ but suggestions are appreciated

This comment has been minimized.

@KotlinIsland KotlinIsland force-pushed the UnionType branch 2 times, most recently from 9f19823 to 58cf572 Compare September 22, 2025 02:24

This comment has been minimized.

@srittau
Copy link
Collaborator

srittau commented Sep 22, 2025

@JelleZijlstra Maybe you could have a look as resident UnionType expert?

stdlib/types.pyi Outdated
Comment on lines 720 to 723
# the `Any` is because of underspecified binop semantics, when the rhs is a `_SpecialForm`
# it might result in a `_SpecialForm` (Union)
def __or__(self, value: Any, /) -> UnionType | type | Any: ...
def __ror__(self, value: Any, /) -> UnionType | type | Any: ...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this comment.

The way we approach this for every other BinOp dunder in typeshed is by annotating the parameters for the reflected dunder on the r.h.s. appropriately. For example, int.__add__ is annotated as only accepting int and only returning int, because int is the only type that can be passed to int.__add__ without int.__add__ returning NotImplemented:

def __add__(self, value: int, /) -> int: ...

But float.__radd__ is annotated as accepting float (interpreted by type checkers as int | float due to the int/float special case), and it is because of this __radd__ method that type checkers are able to infer that 1 + 3.4 should be understood as evaluating to an instance of float:

def __radd__(self, value: float, /) -> float: ...

Why would we do something differently here than what we do for every other BinOp dunder in typeshed?

Copy link
Contributor Author

@KotlinIsland KotlinIsland Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the issue is that UnionType.__or__ accepts Any, so it is clobbering _SpecialForm.__ror__

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, okay, that makes sense. Could you expand the comment a bit to say that?

This comment has been minimized.

Copy link
Contributor

According to mypy_primer, this change has no effect on the checked open source code. 🤖🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants