Notice: While Javascript is not essential for this website, your interaction with the content will be limited. Please turn Javascript on for the full experience.

PEP 604 -- Complementary syntax for Union[]

PEP:604
Title:Complementary syntax for Union[]
Author:Philippe PRADOS <python at prados.fr>
Sponsor:Chris Angelico <rosuav at gmail.com>
BDFL-Delegate:Ivan Levkivskyi <levkivskyi at gmail.com>
Status:Draft
Type:Standards Track
Created:28-Aug-2019
Python-Version:3.9

Abstract

This PEP proposes a complementary syntax for Union[X,Y] and extends its purpose to isinstance and issubclass.

Motivation

PEP 484 and PEP 526 propose a generic syntax to add typing to variables, parameters and function returns. PEP 585 proposes to expose parameters to generics at runtime. MyPy [1] accepts a syntax which looks like:

annotation: name_type
name_type: NAME (args)?
args: '[' paramslist ']'
paramslist: annotation (',' annotation)* [',']
  • To describe a disjunction, the user must use Union[X,Y].

The verbosity of this syntax does not help the adoption.

Proposal

Inspired by Scala language [2] and Pike [3], this proposal adds operator type.__or__(). With this new operator, it is possible to write int | str instead of Union[int,str]. The result of this expression would then be valid in isinstance() and issubclass():

isinstance(5, int | str)
issubclass(bool, int | float)

Examples

Here are some examples of what we can do with this feature.

# in place of
# def f(list: List[Union[int, str]], param: Optional[int]) -> Union[float, str]
def f(list: List[int | str], param: int | None) -> float | str:
    pass

f([1, "abc"], None)

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

assert isinstance("", int | str)
assert issubclass(bool, int | float)

Once the Python language is extended, MyPy [1] and other type checkers will need to be updated to accept this new syntax.

Technical point of view

To accept to extend isinstance() and issubclass(), the object _GenericAlias must be available as a core, that doesn't have a directly-accessible name but via alias in typing module..

Incompatible changes

In some situations, some exceptions will not be raised as expected.

For backward compatibility, typing.py must say _GenericAlias = _GenericAlias.

If a metaclass implements the __or__ operator, it will override this:

>>> class M(type):
...         def __or__(self,other): return "Hello"
...
>>> class C(metaclass=M):pass
...
>>> C | int
'Hello'
>>> int | C
typing.Union[int, __main__.C]
>>> Union[C,int]
typing.Union[__main__.C, int]

Objections and responses

For more details about discussions, see links below:

1. Add a new operator for Union[type1|type2]?

PROS:

  • This syntax can be more readable, and is similary to others languages (Scala, ...)
  • At runtime, int|str might return a simple object in 3.9, rather than everything that you'd need to grab from importing typing

CONS:

  • Adding this operator introduce a dependency between typing and builtins
  • As breaking the backport (in that typing can easily be backported but core types can't)
  • If Python itself doesn't have to be changed, we'd still need to implement it in mypy, Pyre, PyCharm, Pytype, and who knows what else (it's a minor change see "Reference Implementation"

Change only the PEP 484 (Type hints) to accept the syntax type1 | type2 ?

PEP 563 (Postponed Evaluation of Annotations) is enough to accept this proposition, if we accept to not be compatible with the dynamic evaluation of annotations (eval()).

>>> from __future__ import annotations
>>> def foo() -> int | str: pass
...
>>> eval(foo.__annotations__['return'])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
TypeError: unsupported operand type(s) for |: 'type' and 'type'

2. Extend isinstance() and issubclass() to accept Union ?

isinstance(x, str | int) ==> "is x an instance of str or int"

PROS:

  • If they were permitted, then instance checking could use an extremely clean-looking notation
  • The implementation can use the tuple present in Union parameter, without create a new instance

CONS:

  • Must migrate all the typing module in builtin

Reference Implementation

A proposed implementation for cpython is here. A proposed implementation for mypy is here.

Source: https://github.com/python/peps/blob/master/pep-0604.rst