Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
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
6 changes: 1 addition & 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
61 changes: 28 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,8 @@ 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)
self.assertNotIsSubclass(int, Union)
self.assertNotIsSubclass(Union, int)
with self.assertRaises(TypeError):
issubclass(Union[int, str], int)

Expand Down Expand Up @@ -1756,29 +1754,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 +1787,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"cannot create 'types\.UnionType' instances"):
class C(Union[int, str]):
pass

Expand Down Expand Up @@ -1843,7 +1837,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 +4206,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 +4308,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 +4773,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 +8820,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 +8830,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 +8845,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
102 changes: 7 additions & 95 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@
import re as stdlib_re # Avoid confusion with the typing.re namespace on <=3.11
import sys
import types
from types import WrapperDescriptorType, MethodWrapperType, MethodDescriptorType, GenericAlias
from types import (
WrapperDescriptorType,
MethodWrapperType,
MethodDescriptorType,
GenericAlias,
UnionType as Union,
)

from _typing import (
_idfunc,
Expand Down Expand Up @@ -315,22 +321,6 @@ def _deduplicate(params):
return params


def _remove_dups_flatten(parameters):
"""Internal helper for Union creation and substitution.

Flatten Unions among parameters, then remove duplicates.
"""
# Flatten out Union[Union[...], ...].
params = []
for p in parameters:
if isinstance(p, (_UnionGenericAlias, types.UnionType)):
params.extend(p.__args__)
else:
params.append(p)

return tuple(_deduplicate(params))


def _flatten_literal_params(parameters):
"""Internal helper for Literal creation: flatten Literals among parameters."""
params = []
Expand Down Expand Up @@ -657,50 +647,6 @@ class FastConnector(Connection):
item = _type_check(parameters, f'{self} accepts only single type.')
return _GenericAlias(self, (item,))

@_SpecialForm
def Union(self, parameters):
"""Union type; Union[X, Y] means either X or Y.

On Python 3.10 and higher, the | operator
can also be used to denote unions;
X | Y means the same thing to the type checker as Union[X, Y].

To define a union, use e.g. Union[int, str]. Details:
- The arguments must be types and there must be at least one.
- None as an argument is a special case and is replaced by
type(None).
- Unions of unions are flattened, e.g.::

assert Union[Union[int, str], float] == Union[int, str, float]

- Unions of a single argument vanish, e.g.::

assert Union[int] == int # The constructor actually returns int

- Redundant arguments are skipped, e.g.::

assert Union[int, str, int] == Union[int, str]

- When comparing unions, the argument order is ignored, e.g.::

assert Union[int, str] == Union[str, int]

- You cannot subclass or instantiate a union.
- You can use Optional[X] as a shorthand for Union[X, None].
"""
if parameters == ():
raise TypeError("Cannot take a Union of no types.")
if not isinstance(parameters, tuple):
parameters = (parameters,)
msg = "Union[arg, ...]: each arg must be a type."
parameters = tuple(_type_check(p, msg) for p in parameters)
parameters = _remove_dups_flatten(parameters)
if len(parameters) == 1:
return parameters[0]
if len(parameters) == 2 and type(None) in parameters:
return _UnionGenericAlias(self, parameters, name="Optional")
return _UnionGenericAlias(self, parameters)

def _make_union(left, right):
"""Used from the C implementation of TypeVar.

Expand Down Expand Up @@ -1526,40 +1472,6 @@ def __getitem__(self, params):
return self.copy_with(params)


class _UnionGenericAlias(_NotIterable, _GenericAlias, _root=True):
def copy_with(self, params):
return Union[params]

def __eq__(self, other):
if not isinstance(other, (_UnionGenericAlias, types.UnionType)):
return NotImplemented
return set(self.__args__) == set(other.__args__)

def __hash__(self):
return hash(frozenset(self.__args__))

def __repr__(self):
args = self.__args__
if len(args) == 2:
if args[0] is type(None):
return f'typing.Optional[{_type_repr(args[1])}]'
elif args[1] is type(None):
return f'typing.Optional[{_type_repr(args[0])}]'
return super().__repr__()

def __instancecheck__(self, obj):
return self.__subclasscheck__(type(obj))

def __subclasscheck__(self, cls):
for arg in self.__args__:
if issubclass(cls, arg):
return True

def __reduce__(self):
func, (origin, args) = super().__reduce__()
return func, (Union, args)


def _value_and_type_iter(parameters):
return ((p, type(p)) for p in parameters)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Make :data:`typing.Union` an alias for :class:`types.UnionType`. Patch by
Jelle Zijlstra.
Loading