Skip to content

Commit dc6d66f

Browse files
JelleZijlstraAlexWaygoodFidget-Spinnercarljm
authored
gh-105499: Merge typing.Union and types.UnionType (#105511)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com> Co-authored-by: Ken Jin <kenjin@python.org> Co-authored-by: Carl Meyer <carl@oddbird.net>
1 parent e091520 commit dc6d66f

20 files changed

+542
-307
lines changed

Diff for: Doc/deprecations/pending-removal-in-future.rst

+5
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ although there is currently no date scheduled for their removal.
127127

128128
* :class:`typing.Text` (:gh:`92332`).
129129

130+
* The internal class ``typing._UnionGenericAlias`` is no longer used to implement
131+
:class:`typing.Union`. To preserve compatibility with users using this private
132+
class, a compatibility shim will be provided until at least Python 3.17. (Contributed by
133+
Jelle Zijlstra in :gh:`105499`.)
134+
130135
* :class:`unittest.IsolatedAsyncioTestCase`: it is deprecated to return a value
131136
that is not ``None`` from a test case.
132137

Diff for: Doc/library/functools.rst

+3-3
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ The :mod:`functools` module defines the following functions:
518518
... for i, elem in enumerate(arg):
519519
... print(i, elem)
520520

521-
:data:`types.UnionType` and :data:`typing.Union` can also be used::
521+
:class:`typing.Union` can also be used::
522522

523523
>>> @fun.register
524524
... def _(arg: int | float, verbose=False):
@@ -654,8 +654,8 @@ The :mod:`functools` module defines the following functions:
654654
The :func:`register` attribute now supports using type annotations.
655655

656656
.. versionchanged:: 3.11
657-
The :func:`register` attribute now supports :data:`types.UnionType`
658-
and :data:`typing.Union` as type annotations.
657+
The :func:`register` attribute now supports
658+
:class:`typing.Union` as a type annotation.
659659

660660

661661
.. class:: singledispatchmethod(func)

Diff for: Doc/library/stdtypes.rst

+13-8
Original file line numberDiff line numberDiff line change
@@ -5364,7 +5364,7 @@ Union Type
53645364
A union object holds the value of the ``|`` (bitwise or) operation on
53655365
multiple :ref:`type objects <bltin-type-objects>`. These types are intended
53665366
primarily for :term:`type annotations <annotation>`. The union type expression
5367-
enables cleaner type hinting syntax compared to :data:`typing.Union`.
5367+
enables cleaner type hinting syntax compared to subscripting :class:`typing.Union`.
53685368

53695369
.. describe:: X | Y | ...
53705370

@@ -5400,9 +5400,10 @@ enables cleaner type hinting syntax compared to :data:`typing.Union`.
54005400

54015401
int | str == str | int
54025402

5403-
* It is compatible with :data:`typing.Union`::
5403+
* It creates instances of :class:`typing.Union`::
54045404

54055405
int | str == typing.Union[int, str]
5406+
type(int | str) is typing.Union
54065407

54075408
* Optional types can be spelled as a union with ``None``::
54085409

@@ -5428,16 +5429,15 @@ enables cleaner type hinting syntax compared to :data:`typing.Union`.
54285429
TypeError: isinstance() argument 2 cannot be a parameterized generic
54295430

54305431
The user-exposed type for the union object can be accessed from
5431-
:data:`types.UnionType` and used for :func:`isinstance` checks. An object cannot be
5432-
instantiated from the type::
5432+
:class:`typing.Union` and used for :func:`isinstance` checks::
54335433

5434-
>>> import types
5435-
>>> isinstance(int | str, types.UnionType)
5434+
>>> import typing
5435+
>>> isinstance(int | str, typing.Union)
54365436
True
5437-
>>> types.UnionType()
5437+
>>> typing.Union()
54385438
Traceback (most recent call last):
54395439
File "<stdin>", line 1, in <module>
5440-
TypeError: cannot create 'types.UnionType' instances
5440+
TypeError: cannot create 'typing.Union' instances
54415441

54425442
.. note::
54435443
The :meth:`!__or__` method for type objects was added to support the syntax
@@ -5464,6 +5464,11 @@ instantiated from the type::
54645464

54655465
.. versionadded:: 3.10
54665466

5467+
.. versionchanged:: 3.14
5468+
5469+
Union objects are now instances of :class:`typing.Union`. Previously, they were instances
5470+
of :class:`types.UnionType`, which remains an alias for :class:`typing.Union`.
5471+
54675472

54685473
.. _typesother:
54695474

Diff for: Doc/library/types.rst

+4
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,10 @@ Standard names are defined for the following types:
314314

315315
.. versionadded:: 3.10
316316

317+
.. versionchanged:: 3.14
318+
319+
This is now an alias for :class:`typing.Union`.
320+
317321
.. class:: TracebackType(tb_next, tb_frame, tb_lasti, tb_lineno)
318322

319323
The type of traceback objects such as found in ``sys.exception().__traceback__``.

Diff for: Doc/library/typing.rst

+9-1
Original file line numberDiff line numberDiff line change
@@ -1086,7 +1086,7 @@ Special forms
10861086
These can be used as types in annotations. They all support subscription using
10871087
``[]``, but each has a unique syntax.
10881088

1089-
.. data:: Union
1089+
.. class:: Union
10901090

10911091
Union type; ``Union[X, Y]`` is equivalent to ``X | Y`` and means either X or Y.
10921092

@@ -1121,6 +1121,14 @@ These can be used as types in annotations. They all support subscription using
11211121
Unions can now be written as ``X | Y``. See
11221122
:ref:`union type expressions<types-union>`.
11231123

1124+
.. versionchanged:: 3.14
1125+
:class:`types.UnionType` is now an alias for :class:`Union`, and both
1126+
``Union[int, str]`` and ``int | str`` create instances of the same class.
1127+
To check whether an object is a ``Union`` at runtime, use
1128+
``isinstance(obj, Union)``. For compatibility with earlier versions of
1129+
Python, use
1130+
``get_origin(obj) is typing.Union or get_origin(obj) is types.UnionType``.
1131+
11241132
.. data:: Optional
11251133

11261134
``Optional[X]`` is equivalent to ``X | None`` (or ``Union[X, None]``).

Diff for: Doc/whatsnew/3.10.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -722,10 +722,10 @@ PEP 604: New Type Union Operator
722722
723723
A new type union operator was introduced which enables the syntax ``X | Y``.
724724
This provides a cleaner way of expressing 'either type X or type Y' instead of
725-
using :data:`typing.Union`, especially in type hints.
725+
using :class:`typing.Union`, especially in type hints.
726726
727727
In previous versions of Python, to apply a type hint for functions accepting
728-
arguments of multiple types, :data:`typing.Union` was used::
728+
arguments of multiple types, :class:`typing.Union` was used::
729729
730730
def square(number: Union[int, float]) -> Union[int, float]:
731731
return number ** 2

Diff for: Doc/whatsnew/3.11.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -740,8 +740,8 @@ fractions
740740
functools
741741
---------
742742

743-
* :func:`functools.singledispatch` now supports :data:`types.UnionType`
744-
and :data:`typing.Union` as annotations to the dispatch argument.::
743+
* :func:`functools.singledispatch` now supports :class:`types.UnionType`
744+
and :class:`typing.Union` as annotations to the dispatch argument.::
745745

746746
>>> from functools import singledispatch
747747
>>> @singledispatch

Diff for: Include/internal/pycore_unionobject.h

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ PyAPI_FUNC(PyObject *) _Py_union_type_or(PyObject *, PyObject *);
1818
extern PyObject *_Py_subs_parameters(PyObject *, PyObject *, PyObject *, PyObject *);
1919
extern PyObject *_Py_make_parameters(PyObject *);
2020
extern PyObject *_Py_union_args(PyObject *self);
21+
extern PyObject *_Py_union_from_tuple(PyObject *args);
2122

2223
#ifdef __cplusplus
2324
}

Diff for: Lib/functools.py

+5-12
Original file line numberDiff line numberDiff line change
@@ -926,16 +926,11 @@ def dispatch(cls):
926926
dispatch_cache[cls] = impl
927927
return impl
928928

929-
def _is_union_type(cls):
930-
from typing import get_origin, Union
931-
return get_origin(cls) in {Union, UnionType}
932-
933929
def _is_valid_dispatch_type(cls):
934930
if isinstance(cls, type):
935931
return True
936-
from typing import get_args
937-
return (_is_union_type(cls) and
938-
all(isinstance(arg, type) for arg in get_args(cls)))
932+
return (isinstance(cls, UnionType) and
933+
all(isinstance(arg, type) for arg in cls.__args__))
939934

940935
def register(cls, func=None):
941936
"""generic_func.register(cls, func) -> func
@@ -967,7 +962,7 @@ def register(cls, func=None):
967962
from annotationlib import Format, ForwardRef
968963
argname, cls = next(iter(get_type_hints(func, format=Format.FORWARDREF).items()))
969964
if not _is_valid_dispatch_type(cls):
970-
if _is_union_type(cls):
965+
if isinstance(cls, UnionType):
971966
raise TypeError(
972967
f"Invalid annotation for {argname!r}. "
973968
f"{cls!r} not all arguments are classes."
@@ -983,10 +978,8 @@ def register(cls, func=None):
983978
f"{cls!r} is not a class."
984979
)
985980

986-
if _is_union_type(cls):
987-
from typing import get_args
988-
989-
for arg in get_args(cls):
981+
if isinstance(cls, UnionType):
982+
for arg in cls.__args__:
990983
registry[arg] = func
991984
else:
992985
registry[cls] = func

Diff for: Lib/test/test_dataclasses/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2314,7 +2314,7 @@ def test_docstring_one_field_with_default_none(self):
23142314
class C:
23152315
x: Union[int, type(None)] = None
23162316

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

23192319
def test_docstring_list_field(self):
23202320
@dataclass

Diff for: Lib/test/test_functools.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3083,7 +3083,7 @@ def _(arg: typing.Union[int, typing.Iterable[str]]):
30833083
"Invalid annotation for 'arg'."
30843084
)
30853085
self.assertEndsWith(str(exc.exception),
3086-
'typing.Union[int, typing.Iterable[str]] not all arguments are classes.'
3086+
'int | typing.Iterable[str] not all arguments are classes.'
30873087
)
30883088

30893089
def test_invalid_positional_argument(self):

Diff for: Lib/test/test_inspect/test_inspect.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1750,8 +1750,8 @@ class C(metaclass=M):
17501750
class TestFormatAnnotation(unittest.TestCase):
17511751
def test_typing_replacement(self):
17521752
from test.typinganndata.ann_module9 import ann, ann1
1753-
self.assertEqual(inspect.formatannotation(ann), 'Union[List[str], int]')
1754-
self.assertEqual(inspect.formatannotation(ann1), 'Union[List[testModule.typing.A], int]')
1753+
self.assertEqual(inspect.formatannotation(ann), 'List[str] | int')
1754+
self.assertEqual(inspect.formatannotation(ann1), 'List[testModule.typing.A] | int')
17551755

17561756
def test_forwardref(self):
17571757
fwdref = ForwardRef('fwdref')

Diff for: Lib/test/test_pydoc/test_pydoc.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ class C(builtins.object)
133133
c_alias = test.test_pydoc.pydoc_mod.C[int]
134134
list_alias1 = typing.List[int]
135135
list_alias2 = list[int]
136-
type_union1 = typing.Union[int, str]
136+
type_union1 = int | str
137137
type_union2 = int | str
138138
139139
VERSION
@@ -223,7 +223,7 @@ class C(builtins.object)
223223
c_alias = test.test_pydoc.pydoc_mod.C[int]
224224
list_alias1 = typing.List[int]
225225
list_alias2 = list[int]
226-
type_union1 = typing.Union[int, str]
226+
type_union1 = int | str
227227
type_union2 = int | str
228228
229229
Author
@@ -1447,17 +1447,17 @@ def test_generic_alias(self):
14471447
self.assertIn(list.__doc__.strip().splitlines()[0], doc)
14481448

14491449
def test_union_type(self):
1450-
self.assertEqual(pydoc.describe(typing.Union[int, str]), '_UnionGenericAlias')
1450+
self.assertEqual(pydoc.describe(typing.Union[int, str]), 'Union')
14511451
doc = pydoc.render_doc(typing.Union[int, str], renderer=pydoc.plaintext)
1452-
self.assertIn('_UnionGenericAlias in module typing', doc)
1453-
self.assertIn('Union = typing.Union', doc)
1452+
self.assertIn('Union in module typing', doc)
1453+
self.assertIn('class Union(builtins.object)', doc)
14541454
if typing.Union.__doc__:
14551455
self.assertIn(typing.Union.__doc__.strip().splitlines()[0], doc)
14561456

1457-
self.assertEqual(pydoc.describe(int | str), 'UnionType')
1457+
self.assertEqual(pydoc.describe(int | str), 'Union')
14581458
doc = pydoc.render_doc(int | str, renderer=pydoc.plaintext)
1459-
self.assertIn('UnionType in module types object', doc)
1460-
self.assertIn('\nclass UnionType(builtins.object)', doc)
1459+
self.assertIn('Union in module typing', doc)
1460+
self.assertIn('class Union(builtins.object)', doc)
14611461
if not MISSING_C_DOCSTRINGS:
14621462
self.assertIn(types.UnionType.__doc__.strip().splitlines()[0], doc)
14631463

Diff for: Lib/test/test_types.py

+41-9
Original file line numberDiff line numberDiff line change
@@ -709,10 +709,6 @@ def test_or_types_operator(self):
709709
y = int | bool
710710
with self.assertRaises(TypeError):
711711
x < y
712-
# Check that we don't crash if typing.Union does not have a tuple in __args__
713-
y = typing.Union[str, int]
714-
y.__args__ = [str, int]
715-
self.assertEqual(x, y)
716712

717713
def test_hash(self):
718714
self.assertEqual(hash(int | str), hash(str | int))
@@ -727,17 +723,40 @@ class B(metaclass=UnhashableMeta): ...
727723

728724
self.assertEqual((A | B).__args__, (A, B))
729725
union1 = A | B
730-
with self.assertRaises(TypeError):
726+
with self.assertRaisesRegex(TypeError, "unhashable type: 'UnhashableMeta'"):
731727
hash(union1)
732728

733729
union2 = int | B
734-
with self.assertRaises(TypeError):
730+
with self.assertRaisesRegex(TypeError, "unhashable type: 'UnhashableMeta'"):
735731
hash(union2)
736732

737733
union3 = A | int
738-
with self.assertRaises(TypeError):
734+
with self.assertRaisesRegex(TypeError, "unhashable type: 'UnhashableMeta'"):
739735
hash(union3)
740736

737+
def test_unhashable_becomes_hashable(self):
738+
is_hashable = False
739+
class UnhashableMeta(type):
740+
def __hash__(self):
741+
if is_hashable:
742+
return 1
743+
else:
744+
raise TypeError("not hashable")
745+
746+
class A(metaclass=UnhashableMeta): ...
747+
class B(metaclass=UnhashableMeta): ...
748+
749+
union = A | B
750+
self.assertEqual(union.__args__, (A, B))
751+
752+
with self.assertRaisesRegex(TypeError, "not hashable"):
753+
hash(union)
754+
755+
is_hashable = True
756+
757+
with self.assertRaisesRegex(TypeError, "union contains 2 unhashable elements"):
758+
hash(union)
759+
741760
def test_instancecheck_and_subclasscheck(self):
742761
for x in (int | str, typing.Union[int, str]):
743762
with self.subTest(x=x):
@@ -921,7 +940,7 @@ def forward_before(x: ForwardBefore[int]) -> None: ...
921940
self.assertEqual(typing.get_args(typing.get_type_hints(forward_after)['x']),
922941
(int, Forward))
923942
self.assertEqual(typing.get_args(typing.get_type_hints(forward_before)['x']),
924-
(int, Forward))
943+
(Forward, int))
925944

926945
def test_or_type_operator_with_Protocol(self):
927946
class Proto(typing.Protocol):
@@ -1015,9 +1034,14 @@ def __eq__(self, other):
10151034
return 1 / 0
10161035

10171036
bt = BadType('bt', (), {})
1037+
bt2 = BadType('bt2', (), {})
10181038
# Comparison should fail and errors should propagate out for bad types.
1039+
union1 = int | bt
1040+
union2 = int | bt2
1041+
with self.assertRaises(ZeroDivisionError):
1042+
union1 == union2
10191043
with self.assertRaises(ZeroDivisionError):
1020-
list[int] | list[bt]
1044+
bt | bt2
10211045

10221046
union_ga = (list[str] | int, collections.abc.Callable[..., str] | int,
10231047
d | int)
@@ -1060,6 +1084,14 @@ def test_or_type_operator_reference_cycle(self):
10601084
self.assertLessEqual(sys.gettotalrefcount() - before, leeway,
10611085
msg='Check for union reference leak.')
10621086

1087+
def test_instantiation(self):
1088+
with self.assertRaises(TypeError):
1089+
types.UnionType()
1090+
self.assertIs(int, types.UnionType[int])
1091+
self.assertIs(int, types.UnionType[int, int])
1092+
self.assertEqual(int | str, types.UnionType[int, str])
1093+
self.assertEqual(int | typing.ForwardRef("str"), types.UnionType[int, "str"])
1094+
10631095

10641096
class MappingProxyTests(unittest.TestCase):
10651097
mappingproxy = types.MappingProxyType

0 commit comments

Comments
 (0)