python-peps/pep-0604.rst

191 lines
5.6 KiB
ReStructuredText

PEP: 604
Title: Complementary syntax for ``Union[]``
Author: Philippe PRADOS <python@prados.fr>
Sponsor: Chris Angelico <rosuav@gmail.com>
BDFL-Delegate: Ivan Levkivskyi <levkivskyi@gmail.com>
Status: Draft
Type: Standards Track
Content-Type: text/x-rst
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 <https://www.python.org/dev/peps/pep-0585/#id7>`_.
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:
- `Discussion in python-ideas <https://mail.python.org/archives/list/python-ideas@python.org/thread/FCTXGDT2NNKRJQ6CDEPWUXHVG2AAQZZY/>`_
- `Discussion in typing-sig <https://mail.python.org/archives/list/typing-sig@python.org/thread/D5HCB4NT4S3WSK33WI26WZSFEXCEMNHN/>`_
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
<https://github.com/pprados/cpython/tree/PEP604>`_.
A proposed implementation for `mypy is here
<https://github.com/pprados/mypy/tree/PEP604>`_.
References
==========
.. [1] MyPy
http://mypy-lang.org/
.. [2] Scala Union Types
https://dotty.epfl.ch/docs/reference/new-types/union-types.html
.. [3] Pike
http://pike.lysator.liu.se/docs/man/chapter_3.html#3.5
Copyright
=========
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.
..
Local Variables:
mode: indented-text
indent-tabs-mode: nil
sentence-end-double-space: t
fill-column: 70
coding: utf-8
End: