Skip to content

gh-105499: Merge typing.Union and types.UnionType #105511

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 52 commits into from
Mar 4, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
8c8b1dd
Unify UnionType and Union
JelleZijlstra Jun 8, 2023
aba63eb
test_typing succeeds
JelleZijlstra Jun 8, 2023
f2f23a0
Merge remote-tracking branch 'upstream/main' into unifyunion
JelleZijlstra Jun 8, 2023
e75282f
blurb
JelleZijlstra Jun 8, 2023
540b04b
Fix test_types
JelleZijlstra Jun 8, 2023
ae8fa62
Documentation
JelleZijlstra Jun 8, 2023
7569c48
stray f
JelleZijlstra Jun 8, 2023
8bcb930
Fix tests
JelleZijlstra Jun 8, 2023
291953e
No more typing._make_union
JelleZijlstra Jun 8, 2023
f7ca8d4
Add tp_new
JelleZijlstra Jun 9, 2023
0f1ab18
fix a refleak
JelleZijlstra Jun 9, 2023
eb47a0b
Fix test
JelleZijlstra Jun 9, 2023
eaa4e79
Update Lib/test/test_typing.py
JelleZijlstra Jun 9, 2023
4a0235f
Merge remote-tracking branch 'upstream/main' into unifyunion
JelleZijlstra Jun 13, 2023
c71e8c3
Add back _UnionGenericAlias
JelleZijlstra Jun 13, 2023
0475415
Merge remote-tracking branch 'upstream/main' into unifyunion
JelleZijlstra Jun 16, 2023
bb50899
Remove unnecessary NewRef
JelleZijlstra Jun 16, 2023
c564672
Make typing.Union the canonical name
JelleZijlstra Jun 16, 2023
9f58421
docs
JelleZijlstra Jun 16, 2023
8884a94
fix test_pydoc
JelleZijlstra Jun 16, 2023
bcc2e6a
Update Objects/unionobject.c
JelleZijlstra Jun 16, 2023
c4b217b
Update Doc/library/typing.rst
JelleZijlstra Jun 17, 2023
5eb2a0c
Update Doc/library/stdtypes.rst
JelleZijlstra Jun 17, 2023
64ac293
Update Doc/library/stdtypes.rst
JelleZijlstra Jun 17, 2023
0ba8551
Merge remote-tracking branch 'upstream/main' into unifyunion
JelleZijlstra Jun 17, 2023
aacf2b0
Add __mro_entries__
JelleZijlstra Jun 17, 2023
f46c0c6
Improve docs, expose it in _typing
JelleZijlstra Jun 17, 2023
8c04441
Update Doc/library/functools.rst
JelleZijlstra Jun 20, 2023
04df4d0
Merge branch 'main' into unifyunion
JelleZijlstra Jun 20, 2023
c363de7
Merge remote-tracking branch 'upstream/main' into unifyunion
JelleZijlstra Oct 11, 2023
98b634a
Post-merge cleanup
JelleZijlstra Oct 11, 2023
f2f961b
No need for tp_new
JelleZijlstra Oct 12, 2023
932f8e5
Merge branch 'main' into unifyunion
JelleZijlstra Oct 28, 2023
c3edd87
Merge remote-tracking branch 'upstream/main' into unifyunion
JelleZijlstra Feb 18, 2024
9ae102d
Add back doctest
JelleZijlstra Feb 18, 2024
e0924c6
Merge branch 'main' into unifyunion
JelleZijlstra Feb 19, 2024
7086879
Merge branch 'main' into unifyunion
JelleZijlstra Feb 28, 2024
b0e057f
Merge branch 'main' into unifyunion
JelleZijlstra Mar 12, 2024
55d0c97
Merge branch 'main' into unifyunion
JelleZijlstra Apr 26, 2024
8a80fe0
Fix two issues
JelleZijlstra Apr 26, 2024
f910e88
in progress: hashable unions
JelleZijlstra Apr 26, 2024
cae36c7
Merge remote-tracking branch 'upstream/main' into unifyunion
JelleZijlstra Sep 28, 2024
65da3f1
fixup
JelleZijlstra Sep 28, 2024
f500f5f
Make union support unhashable objects
JelleZijlstra Sep 28, 2024
5f9e599
simplify, extend docs
JelleZijlstra Sep 28, 2024
9927b38
fix more tests
JelleZijlstra Sep 28, 2024
eea6eca
another
JelleZijlstra Sep 28, 2024
0785951
change hash
JelleZijlstra Sep 29, 2024
1a39ded
Merge remote-tracking branch 'upstream/main' into unifyunion
JelleZijlstra Sep 29, 2024
eddbfde
Merge branch 'main' into unifyunion
JelleZijlstra Nov 5, 2024
3ec9271
Merge branch 'main' into unifyunion
JelleZijlstra Mar 3, 2025
cab69f0
tweak docs
JelleZijlstra Mar 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,9 @@ These can be used as types in annotations using ``[]``, each having a unique syn
Unions can now be written as ``X | Y``. See
:ref:`union type expressions<types-union>`.

.. versionchanged:: 3.13
:data:`Union` is now implemented as an alias of :class:`types.UnionType`.

.. data:: Optional

Optional type.
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_unionobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ extern PyObject *_Py_union_type_or(PyObject *, PyObject *);
extern PyObject *_Py_subs_parameters(PyObject *, PyObject *, PyObject *, PyObject *);
extern PyObject *_Py_make_parameters(PyObject *);
extern PyObject *_Py_union_args(PyObject *self);
extern PyObject *_Py_union_from_tuple(PyObject *args);

#ifdef __cplusplus
}
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2267,7 +2267,7 @@ def test_docstring_one_field_with_default_none(self):
class C:
x: Union[int, type(None)] = None

self.assertDocStrEqual(C.__doc__, "C(x:Optional[int]=None)")
self.assertDocStrEqual(C.__doc__, "C(x:int|None=None)")

def test_docstring_list_field(self):
@dataclass
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -2774,7 +2774,7 @@ def _(arg: typing.Union[int, typing.Iterable[str]]):
"Invalid annotation for 'arg'."
))
self.assertTrue(str(exc.exception).endswith(
'typing.Union[int, typing.Iterable[str]] not all arguments are classes.'
'int | typing.Iterable[str] not all arguments are classes.'
))

def test_invalid_positional_argument(self):
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -1507,8 +1507,8 @@ def wrapper(a, b):
class TestFormatAnnotation(unittest.TestCase):
def test_typing_replacement(self):
from test.typinganndata.ann_module9 import ann, ann1
self.assertEqual(inspect.formatannotation(ann), 'Union[List[str], int]')
self.assertEqual(inspect.formatannotation(ann1), 'Union[List[testModule.typing.A], int]')
self.assertEqual(inspect.formatannotation(ann), 'typing.List[str] | int')
self.assertEqual(inspect.formatannotation(ann1), 'typing.List[testModule.typing.A] | int')


class TestIsDataDescriptor(unittest.TestCase):
Expand Down
14 changes: 7 additions & 7 deletions Lib/test/test_pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class C(builtins.object)
c_alias = test.pydoc_mod.C[int]
list_alias1 = typing.List[int]
list_alias2 = list[int]
type_union1 = typing.Union[int, str]
type_union1 = int | str
type_union2 = int | str

VERSION
Expand Down Expand Up @@ -208,7 +208,7 @@ class C(builtins.object)
c_alias = test.pydoc_mod.C[int]
list_alias1 = typing.List[int]
list_alias2 = list[int]
type_union1 = typing.Union[int, str]
type_union1 = int | str
type_union2 = int | str

Author
Expand Down Expand Up @@ -1055,17 +1055,17 @@ def test_generic_alias(self):
self.assertIn(list.__doc__.strip().splitlines()[0], doc)

def test_union_type(self):
self.assertEqual(pydoc.describe(typing.Union[int, str]), '_UnionGenericAlias')
self.assertEqual(pydoc.describe(typing.Union[int, str]), 'UnionType')
doc = pydoc.render_doc(typing.Union[int, str], renderer=pydoc.plaintext)
self.assertIn('_UnionGenericAlias in module typing', doc)
self.assertIn('Union = typing.Union', doc)
self.assertIn('UnionType in module types', doc)
self.assertIn('class UnionType(builtins.object)', doc)
if typing.Union.__doc__:
self.assertIn(typing.Union.__doc__.strip().splitlines()[0], doc)

self.assertEqual(pydoc.describe(int | str), 'UnionType')
doc = pydoc.render_doc(int | str, renderer=pydoc.plaintext)
self.assertIn('UnionType in module types object', doc)
self.assertIn('\nclass UnionType(builtins.object)', doc)
self.assertIn('UnionType in module types', doc)
self.assertIn('class UnionType(builtins.object)', doc)
self.assertIn(types.UnionType.__doc__.strip().splitlines()[0], doc)

def test_special_form(self):
Expand Down
14 changes: 9 additions & 5 deletions Lib/test/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -698,10 +698,6 @@ def test_or_types_operator(self):
y = int | bool
with self.assertRaises(TypeError):
x < y
# Check that we don't crash if typing.Union does not have a tuple in __args__
y = typing.Union[str, int]
y.__args__ = [str, int]
Copy link
Member Author

Choose a reason for hiding this comment

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

.__args__ is no longer writable.

self.assertEqual(x, y)

def test_hash(self):
self.assertEqual(hash(int | str), hash(str | int))
Expand Down Expand Up @@ -890,7 +886,7 @@ def forward_before(x: ForwardBefore[int]) -> None: ...
self.assertEqual(typing.get_args(typing.get_type_hints(forward_after)['x']),
(int, Forward))
self.assertEqual(typing.get_args(typing.get_type_hints(forward_before)['x']),
(int, Forward))
(Forward, int))

def test_or_type_operator_with_Protocol(self):
class Proto(typing.Protocol):
Expand Down Expand Up @@ -1029,6 +1025,14 @@ def test_or_type_operator_reference_cycle(self):
self.assertLessEqual(sys.gettotalrefcount() - before, leeway,
msg='Check for union reference leak.')

def test_instantiation(self):
with self.assertRaises(TypeError):
types.UnionType()
self.assertIs(int, types.UnionType(int))
self.assertIs(int, types.UnionType(int, int))
self.assertEqual(int | str, types.UnionType(int, str))
self.assertEqual(int | typing.ForwardRef("str"), types.UnionType(int, "str"))


class MappingProxyTests(unittest.TestCase):
mappingproxy = types.MappingProxyType
Expand Down
59 changes: 26 additions & 33 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ def test_cannot_instantiate_vars(self):

def test_bound_errors(self):
with self.assertRaises(TypeError):
TypeVar('X', bound=Union)
TypeVar('X', bound=Optional)
with self.assertRaises(TypeError):
TypeVar('X', str, float, bound=Employee)
with self.assertRaisesRegex(TypeError,
Expand Down Expand Up @@ -531,7 +531,7 @@ def test_var_substitution(self):
def test_bad_var_substitution(self):
T = TypeVar('T')
bad_args = (
(), (int, str), Union,
(), (int, str), Optional,
Generic, Generic[T], Protocol, Protocol[T],
Final, Final[int], ClassVar, ClassVar[int],
)
Expand Down Expand Up @@ -1708,10 +1708,6 @@ def test_basics(self):
self.assertNotEqual(u, Union)

def test_subclass_error(self):
with self.assertRaises(TypeError):
issubclass(int, Union)
with self.assertRaises(TypeError):
issubclass(Union, int)
with self.assertRaises(TypeError):
issubclass(Union[int, str], int)

Expand Down Expand Up @@ -1756,29 +1752,28 @@ def test_union_union(self):
self.assertEqual(v, Union[int, float, Employee])

def test_repr(self):
self.assertEqual(repr(Union), 'typing.Union')
u = Union[Employee, int]
self.assertEqual(repr(u), 'typing.Union[%s.Employee, int]' % __name__)
self.assertEqual(repr(u), f'{__name__}.Employee | int')
u = Union[int, Employee]
self.assertEqual(repr(u), 'typing.Union[int, %s.Employee]' % __name__)
self.assertEqual(repr(u), f'int | {__name__}.Employee')
T = TypeVar('T')
u = Union[T, int][int]
self.assertEqual(repr(u), repr(int))
u = Union[List[int], int]
self.assertEqual(repr(u), 'typing.Union[typing.List[int], int]')
self.assertEqual(repr(u), 'typing.List[int] | int')
u = Union[list[int], dict[str, float]]
self.assertEqual(repr(u), 'typing.Union[list[int], dict[str, float]]')
self.assertEqual(repr(u), 'list[int] | dict[str, float]')
u = Union[int | float]
self.assertEqual(repr(u), 'typing.Union[int, float]')
self.assertEqual(repr(u), 'int | float')

u = Union[None, str]
self.assertEqual(repr(u), 'typing.Optional[str]')
self.assertEqual(repr(u), 'None | str')
u = Union[str, None]
self.assertEqual(repr(u), 'typing.Optional[str]')
self.assertEqual(repr(u), 'str | None')
u = Union[None, str, int]
self.assertEqual(repr(u), 'typing.Union[NoneType, str, int]')
self.assertEqual(repr(u), 'None | str | int')
u = Optional[str]
self.assertEqual(repr(u), 'typing.Optional[str]')
self.assertEqual(repr(u), 'str | None')

def test_dir(self):
dir_items = set(dir(Union[str, int]))
Expand All @@ -1790,14 +1785,11 @@ def test_dir(self):

def test_cannot_subclass(self):
with self.assertRaisesRegex(TypeError,
r'Cannot subclass typing\.Union'):
r"type 'types\.UnionType' is not an acceptable base type"):
class C(Union):
pass
with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE):
class C(type(Union)):
pass
with self.assertRaisesRegex(TypeError,
r'Cannot subclass typing\.Union\[int, str\]'):
r"Union\[arg, \.\.\.\]: each arg must be a type\."):
class C(Union[int, str]):
pass

Expand Down Expand Up @@ -1843,7 +1835,7 @@ def f(x: u): ...

def test_function_repr_union(self):
def fun() -> int: ...
self.assertEqual(repr(Union[fun, int]), 'typing.Union[fun, int]')
self.assertEqual(repr(Union[fun, int]), f'{__name__}.{fun.__qualname__} | int')

def test_union_str_pattern(self):
# Shouldn't crash; see http://bugs.python.org/issue25390
Expand Down Expand Up @@ -4212,11 +4204,11 @@ class Derived(Base): ...
def test_extended_generic_rules_repr(self):
T = TypeVar('T')
self.assertEqual(repr(Union[Tuple, Callable]).replace('typing.', ''),
'Union[Tuple, Callable]')
'Tuple | Callable')
self.assertEqual(repr(Union[Tuple, Tuple[int]]).replace('typing.', ''),
'Union[Tuple, Tuple[int]]')
'Tuple | Tuple[int]')
self.assertEqual(repr(Callable[..., Optional[T]][int]).replace('typing.', ''),
'Callable[..., Optional[int]]')
'Callable[..., int | None]')
self.assertEqual(repr(Callable[[], List[T]][int]).replace('typing.', ''),
'Callable[[], List[int]]')

Expand Down Expand Up @@ -4314,9 +4306,9 @@ def __contains__(self, item):
with self.assertRaises(TypeError):
issubclass(Tuple[int, ...], typing.Iterable)

def test_fail_with_bare_union(self):
def test_fail_with_special_forms(self):
with self.assertRaises(TypeError):
List[Union]
List[Final]
with self.assertRaises(TypeError):
Tuple[Optional]
with self.assertRaises(TypeError):
Expand Down Expand Up @@ -4779,8 +4771,6 @@ def test_subclass_special_form(self):
for obj in (
ClassVar[int],
Final[int],
Union[int, float],
Optional[int],
Literal[1, 2],
Concatenate[int, ParamSpec("P")],
TypeGuard[int],
Expand Down Expand Up @@ -8828,7 +8818,6 @@ def test_special_attrs(self):
typing.TypeAlias: 'TypeAlias',
typing.TypeGuard: 'TypeGuard',
typing.TypeVar: 'TypeVar',
typing.Union: 'Union',
typing.Self: 'Self',
# Subscribed special forms
typing.Annotated[Any, "Annotation"]: 'Annotated',
Expand All @@ -8839,7 +8828,7 @@ def test_special_attrs(self):
typing.Literal[Any]: 'Literal',
typing.Literal[1, 2]: 'Literal',
typing.Literal[True, 2]: 'Literal',
typing.Optional[Any]: 'Optional',
typing.Optional[Any]: 'Union',
typing.TypeGuard[Any]: 'TypeGuard',
typing.Union[Any]: 'Any',
typing.Union[int, float]: 'Union',
Expand All @@ -8854,11 +8843,15 @@ def test_special_attrs(self):
with self.subTest(cls=cls):
self.assertEqual(cls.__name__, name, str(cls))
self.assertEqual(cls.__qualname__, name, str(cls))
self.assertEqual(cls.__module__, 'typing', str(cls))
mod = 'types' if isinstance(cls, types.UnionType) else 'typing'
self.assertEqual(cls.__module__, mod, str(cls))
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
s = pickle.dumps(cls, proto)
loaded = pickle.loads(s)
self.assertIs(cls, loaded)
if isinstance(cls, types.UnionType):
self.assertEqual(cls, loaded)
else:
self.assertIs(cls, loaded)

TypeName = typing.NewType('SpecialAttrsTests.TypeName', Any)

Expand Down
Loading