Skip to content

Commit d4dc00f

Browse files
cdce8pppradoshauntsaninja
authored
Add support for union types as X | Y (PEP 604) (#9647)
Co-authored-by: Philippe Prados <[email protected]> Co-authored-by: Shantanu <[email protected]>
1 parent 37c4e2f commit d4dc00f

File tree

8 files changed

+267
-6
lines changed

8 files changed

+267
-6
lines changed

docs/source/kinds_of_types.rst

+90
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,96 @@ more specific type:
241241
since the caller may have to use :py:func:`isinstance` before doing anything
242242
interesting with the value.
243243

244+
.. _alternative_union_syntax:
245+
246+
Alternative union syntax
247+
------------------------
248+
249+
`PEP 604 <https://www.python.org/dev/peps/pep-0604/>`_ introduced an alternative way
250+
for writing union types. Starting with **Python 3.10** it is possible to write
251+
``Union[int, str]`` as ``int | str``. Any of the following options is possible
252+
253+
.. code-block:: python
254+
255+
from typing import List
256+
257+
# Use as Union
258+
t1: int | str # equivalent to Union[int, str]
259+
260+
# Use as Optional
261+
t2: int | None # equivalent to Optional[int]
262+
263+
# Use in generics
264+
t3: List[int | str] # equivalent to List[Union[int, str]]
265+
266+
# Use in type aliases
267+
T4 = int | None
268+
x: T4
269+
270+
# Quoted variable annotations
271+
t5: "int | str"
272+
273+
# Quoted function annotations
274+
def f(t6: "int | str") -> None: ...
275+
276+
# Type comments
277+
t6 = 42 # type: int | str
278+
279+
It is possible to use most of these even for earlier versions. However there are some
280+
limitations to be aware of.
281+
282+
.. _alternative_union_syntax_stub_files:
283+
284+
Stub files
285+
""""""""""
286+
287+
All options are supported, regardless of the Python version the project uses.
288+
289+
.. _alternative_union_syntax_37:
290+
291+
Python 3.7 - 3.9
292+
""""""""""""""""
293+
294+
It is necessary to add ``from __future__ import annotations`` to delay the evaluation
295+
of type annotations. Not using it would result in a ``TypeError``.
296+
This does not apply for **type comments**, **quoted function** and **quoted variable** annotations,
297+
as those also work for earlier versions, see :ref:`below <alternative_union_syntax_older_version>`.
298+
299+
.. warning::
300+
301+
Type aliases are **NOT** supported! Those result in a ``TypeError`` regardless
302+
if the evaluation of type annotations is delayed.
303+
304+
Dynamic evaluation of annotations is **NOT** possible (e.g. ``typing.get_type_hints`` and ``eval``).
305+
See `note PEP 604 <https://www.python.org/dev/peps/pep-0604/#change-only-pep-484-type-hints-to-accept-the-syntax-type1-type2>`_.
306+
Use ``typing.Union`` or **Python 3.10** instead if you need those!
307+
308+
.. code-block:: python
309+
310+
from __future__ import annotations
311+
312+
t1: int | None
313+
314+
# Type aliases
315+
T2 = int | None # TypeError!
316+
317+
.. _alternative_union_syntax_older_version:
318+
319+
Older versions
320+
""""""""""""""
321+
322+
+------------------------------------------+-----------+-----------+-----------+
323+
| Python Version | 3.6 | 3.0 - 3.5 | 2.7 |
324+
+==========================================+===========+===========+===========+
325+
| Type comments | yes | yes | yes |
326+
+------------------------------------------+-----------+-----------+-----------+
327+
| Quoted function annotations | yes | yes | |
328+
+------------------------------------------+-----------+-----------+-----------+
329+
| Quoted variable annotations | yes | | |
330+
+------------------------------------------+-----------+-----------+-----------+
331+
| Everything else | | | |
332+
+------------------------------------------+-----------+-----------+-----------+
333+
244334
.. _strict_optional:
245335

246336
Optional types and the None type

mypy/fastparse.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
)
3232
from mypy.types import (
3333
Type, CallableType, AnyType, UnboundType, TupleType, TypeList, EllipsisType, CallableArgument,
34-
TypeOfAny, Instance, RawExpressionType, ProperType
34+
TypeOfAny, Instance, RawExpressionType, ProperType, UnionType,
3535
)
3636
from mypy import defaults
3737
from mypy import message_registry, errorcodes as codes
@@ -241,7 +241,8 @@ def parse_type_comment(type_comment: str,
241241
converted = TypeConverter(errors,
242242
line=line,
243243
override_column=column,
244-
assume_str_is_unicode=assume_str_is_unicode).visit(typ.body)
244+
assume_str_is_unicode=assume_str_is_unicode,
245+
is_evaluated=False).visit(typ.body)
245246
return ignored, converted
246247

247248

@@ -268,6 +269,8 @@ def parse_type_string(expr_string: str, expr_fallback_name: str,
268269
node.original_str_expr = expr_string
269270
node.original_str_fallback = expr_fallback_name
270271
return node
272+
elif isinstance(node, UnionType):
273+
return node
271274
else:
272275
return RawExpressionType(expr_string, expr_fallback_name, line, column)
273276
except (SyntaxError, ValueError):
@@ -1276,12 +1279,14 @@ def __init__(self,
12761279
line: int = -1,
12771280
override_column: int = -1,
12781281
assume_str_is_unicode: bool = True,
1282+
is_evaluated: bool = True,
12791283
) -> None:
12801284
self.errors = errors
12811285
self.line = line
12821286
self.override_column = override_column
12831287
self.node_stack = [] # type: List[AST]
12841288
self.assume_str_is_unicode = assume_str_is_unicode
1289+
self.is_evaluated = is_evaluated
12851290

12861291
def convert_column(self, column: int) -> int:
12871292
"""Apply column override if defined; otherwise return column.
@@ -1422,6 +1427,18 @@ def _extract_argument_name(self, n: ast3.expr) -> Optional[str]:
14221427
def visit_Name(self, n: Name) -> Type:
14231428
return UnboundType(n.id, line=self.line, column=self.convert_column(n.col_offset))
14241429

1430+
def visit_BinOp(self, n: ast3.BinOp) -> Type:
1431+
if not isinstance(n.op, ast3.BitOr):
1432+
return self.invalid_type(n)
1433+
1434+
left = self.visit(n.left)
1435+
right = self.visit(n.right)
1436+
return UnionType([left, right],
1437+
line=self.line,
1438+
column=self.convert_column(n.col_offset),
1439+
is_evaluated=self.is_evaluated,
1440+
uses_pep604_syntax=True)
1441+
14251442
def visit_NameConstant(self, n: NameConstant) -> Type:
14261443
if isinstance(n.value, bool):
14271444
return RawExpressionType(n.value, 'builtins.bool', line=self.line)

mypy/semanal.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ class SemanticAnalyzer(NodeVisitor[None],
206206
patches = None # type: List[Tuple[int, Callable[[], None]]]
207207
loop_depth = 0 # Depth of breakable loops
208208
cur_mod_id = '' # Current module id (or None) (phase 2)
209-
is_stub_file = False # Are we analyzing a stub file?
209+
_is_stub_file = False # Are we analyzing a stub file?
210210
_is_typeshed_stub_file = False # Are we analyzing a typeshed stub file?
211211
imports = None # type: Set[str] # Imported modules (during phase 2 analysis)
212212
# Note: some imports (and therefore dependencies) might
@@ -280,6 +280,10 @@ def __init__(self,
280280

281281
# mypyc doesn't properly handle implementing an abstractproperty
282282
# with a regular attribute so we make them properties
283+
@property
284+
def is_stub_file(self) -> bool:
285+
return self._is_stub_file
286+
283287
@property
284288
def is_typeshed_stub_file(self) -> bool:
285289
return self._is_typeshed_stub_file
@@ -507,7 +511,7 @@ def file_context(self,
507511
self.cur_mod_node = file_node
508512
self.cur_mod_id = file_node.fullname
509513
scope.enter_file(self.cur_mod_id)
510-
self.is_stub_file = file_node.path.lower().endswith('.pyi')
514+
self._is_stub_file = file_node.path.lower().endswith('.pyi')
511515
self._is_typeshed_stub_file = is_typeshed_file(file_node.path)
512516
self.globals = file_node.names
513517
self.tvar_scope = TypeVarLikeScope()

mypy/semanal_shared.py

+5
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ def is_future_flag_set(self, flag: str) -> bool:
7878
"""Is the specific __future__ feature imported"""
7979
raise NotImplementedError
8080

81+
@property
82+
@abstractmethod
83+
def is_stub_file(self) -> bool:
84+
raise NotImplementedError
85+
8186

8287
@trait
8388
class SemanticAnalyzerInterface(SemanticAnalyzerCoreInterface):

mypy/test/testcheck.py

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
# List of files that contain test case descriptions.
2626
typecheck_files = [
2727
'check-basic.test',
28+
'check-union-or-syntax.test',
2829
'check-callable.test',
2930
'check-classes.test',
3031
'check-statements.test',

mypy/typeanal.py

+6
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,12 @@ def visit_star_type(self, t: StarType) -> Type:
608608
return StarType(self.anal_type(t.type), t.line)
609609

610610
def visit_union_type(self, t: UnionType) -> Type:
611+
if (t.uses_pep604_syntax is True
612+
and t.is_evaluated is True
613+
and self.api.is_stub_file is False
614+
and self.options.python_version < (3, 10)
615+
and self.api.is_future_flag_set('annotations') is False):
616+
self.fail("X | Y syntax for unions requires Python 3.10", t)
611617
return UnionType(self.anal_array(t.items), t.line)
612618

613619
def visit_partial_type(self, t: PartialType) -> Type:

mypy/types.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -1722,13 +1722,18 @@ def serialize(self) -> JsonDict:
17221722
class UnionType(ProperType):
17231723
"""The union type Union[T1, ..., Tn] (at least one type argument)."""
17241724

1725-
__slots__ = ('items',)
1725+
__slots__ = ('items', 'is_evaluated', 'uses_pep604_syntax')
17261726

1727-
def __init__(self, items: Sequence[Type], line: int = -1, column: int = -1) -> None:
1727+
def __init__(self, items: Sequence[Type], line: int = -1, column: int = -1,
1728+
is_evaluated: bool = True, uses_pep604_syntax: bool = False) -> None:
17281729
super().__init__(line, column)
17291730
self.items = flatten_nested_unions(items)
17301731
self.can_be_true = any(item.can_be_true for item in items)
17311732
self.can_be_false = any(item.can_be_false for item in items)
1733+
# is_evaluated should be set to false for type comments and string literals
1734+
self.is_evaluated = is_evaluated
1735+
# uses_pep604_syntax is True if Union uses OR syntax (X | Y)
1736+
self.uses_pep604_syntax = uses_pep604_syntax
17321737

17331738
def __hash__(self) -> int:
17341739
return hash(frozenset(self.items))
+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
-- Type checking of union types with '|' syntax
2+
3+
[case testUnionOrSyntaxWithTwoBuiltinsTypes]
4+
# flags: --python-version 3.10
5+
from __future__ import annotations
6+
def f(x: int | str) -> int | str:
7+
reveal_type(x) # N: Revealed type is 'Union[builtins.int, builtins.str]'
8+
z: int | str = 0
9+
reveal_type(z) # N: Revealed type is 'Union[builtins.int, builtins.str]'
10+
return x
11+
reveal_type(f) # N: Revealed type is 'def (x: Union[builtins.int, builtins.str]) -> Union[builtins.int, builtins.str]'
12+
[builtins fixtures/tuple.pyi]
13+
14+
15+
[case testUnionOrSyntaxWithThreeBuiltinsTypes]
16+
# flags: --python-version 3.10
17+
def f(x: int | str | float) -> int | str | float:
18+
reveal_type(x) # N: Revealed type is 'Union[builtins.int, builtins.str, builtins.float]'
19+
z: int | str | float = 0
20+
reveal_type(z) # N: Revealed type is 'Union[builtins.int, builtins.str, builtins.float]'
21+
return x
22+
reveal_type(f) # N: Revealed type is 'def (x: Union[builtins.int, builtins.str, builtins.float]) -> Union[builtins.int, builtins.str, builtins.float]'
23+
24+
25+
[case testUnionOrSyntaxWithTwoTypes]
26+
# flags: --python-version 3.10
27+
class A: pass
28+
class B: pass
29+
def f(x: A | B) -> A | B:
30+
reveal_type(x) # N: Revealed type is 'Union[__main__.A, __main__.B]'
31+
z: A | B = A()
32+
reveal_type(z) # N: Revealed type is 'Union[__main__.A, __main__.B]'
33+
return x
34+
reveal_type(f) # N: Revealed type is 'def (x: Union[__main__.A, __main__.B]) -> Union[__main__.A, __main__.B]'
35+
36+
37+
[case testUnionOrSyntaxWithThreeTypes]
38+
# flags: --python-version 3.10
39+
class A: pass
40+
class B: pass
41+
class C: pass
42+
def f(x: A | B | C) -> A | B | C:
43+
reveal_type(x) # N: Revealed type is 'Union[__main__.A, __main__.B, __main__.C]'
44+
z: A | B | C = A()
45+
reveal_type(z) # N: Revealed type is 'Union[__main__.A, __main__.B, __main__.C]'
46+
return x
47+
reveal_type(f) # N: Revealed type is 'def (x: Union[__main__.A, __main__.B, __main__.C]) -> Union[__main__.A, __main__.B, __main__.C]'
48+
49+
50+
[case testUnionOrSyntaxWithLiteral]
51+
# flags: --python-version 3.10
52+
from typing_extensions import Literal
53+
reveal_type(Literal[4] | str) # N: Revealed type is 'Any'
54+
[builtins fixtures/tuple.pyi]
55+
56+
57+
[case testUnionOrSyntaxWithBadOperator]
58+
# flags: --python-version 3.10
59+
x: 1 + 2 # E: Invalid type comment or annotation
60+
61+
62+
[case testUnionOrSyntaxWithBadOperands]
63+
# flags: --python-version 3.10
64+
x: int | 42 # E: Invalid type: try using Literal[42] instead?
65+
y: 42 | int # E: Invalid type: try using Literal[42] instead?
66+
z: str | 42 | int # E: Invalid type: try using Literal[42] instead?
67+
68+
69+
[case testUnionOrSyntaxWithGenerics]
70+
# flags: --python-version 3.10
71+
from typing import List
72+
x: List[int | str]
73+
reveal_type(x) # N: Revealed type is 'builtins.list[Union[builtins.int, builtins.str]]'
74+
[builtins fixtures/list.pyi]
75+
76+
77+
[case testUnionOrSyntaxWithQuotedFunctionTypes]
78+
# flags: --python-version 3.4
79+
from typing import Union
80+
def f(x: 'Union[int, str, None]') -> 'Union[int, None]':
81+
reveal_type(x) # N: Revealed type is 'Union[builtins.int, builtins.str, None]'
82+
return 42
83+
reveal_type(f) # N: Revealed type is 'def (x: Union[builtins.int, builtins.str, None]) -> Union[builtins.int, None]'
84+
85+
def g(x: "int | str | None") -> "int | None":
86+
reveal_type(x) # N: Revealed type is 'Union[builtins.int, builtins.str, None]'
87+
return 42
88+
reveal_type(g) # N: Revealed type is 'def (x: Union[builtins.int, builtins.str, None]) -> Union[builtins.int, None]'
89+
90+
91+
[case testUnionOrSyntaxWithQuotedVariableTypes]
92+
# flags: --python-version 3.6
93+
y: "int | str" = 42
94+
reveal_type(y) # N: Revealed type is 'Union[builtins.int, builtins.str]'
95+
96+
97+
[case testUnionOrSyntaxWithTypeAliasWorking]
98+
# flags: --python-version 3.10
99+
from typing import Union
100+
T = Union[int, str]
101+
x: T
102+
reveal_type(x) # N: Revealed type is 'Union[builtins.int, builtins.str]'
103+
104+
105+
[case testUnionOrSyntaxWithTypeAliasNotAllowed]
106+
# flags: --python-version 3.9
107+
from __future__ import annotations
108+
T = int | str # E: Unsupported left operand type for | ("Type[int]")
109+
[builtins fixtures/tuple.pyi]
110+
111+
112+
[case testUnionOrSyntaxInComment]
113+
# flags: --python-version 3.6
114+
x = 1 # type: int | str
115+
116+
117+
[case testUnionOrSyntaxFutureImport]
118+
# flags: --python-version 3.7
119+
from __future__ import annotations
120+
x: int | None
121+
[builtins fixtures/tuple.pyi]
122+
123+
124+
[case testUnionOrSyntaxMissingFutureImport]
125+
# flags: --python-version 3.9
126+
x: int | None # E: X | Y syntax for unions requires Python 3.10
127+
128+
129+
[case testUnionOrSyntaxInStubFile]
130+
# flags: --python-version 3.6
131+
from lib import x
132+
[file lib.pyi]
133+
x: int | None

0 commit comments

Comments
 (0)