Skip to content
212 changes: 212 additions & 0 deletions pep-9999.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
PEP: 9999
Title: Inline variance operators for generic type variables: ``+T`` and ``-T``
Author: Joren Hammudoglu <jhammudoglu at gmail.com>
Status: Draft
Type: Standards Track
Content-Type: text/x-rst
Created: 24-Jul-2021
Post-History: 24-Jul-2021


Abstract
========

PEP 484 proposes a syntax for defining the `covariance and contravariance
<https://www.python.org/dev/peps/pep-0484/#covariance-and-contravariance>`_
of `user-defined generic types
<https://www.python.org/dev/peps/pep-0484/#user-defined-generic-types>`_::

T_co = typing.TypeVar('T_co', covariant=True)
T_contra = typing.TypeVar('T_contra', contravariant=True)


# a covariant generic type
class EventDispatcher(typing.Generic[T_co]):
def dispatch() -> T_co: ...

# a contravariant generic type
class EventListener(typing.Generic[T_contra]):
def receive(event: T_contra) -> None: ...


This PEP proposes a concise syntax for specifying the variance of
generic types by overloading the ``+`` and ``-`` prefix operators
on type variables::

T = typing.TypeVar('T')


# a covariant generic type
class EventDispatcher(typing.Generic[+T]):
def dispatch() -> T: ...

# a contravariant generic type
class EventListener(typing.Generic[-T]):
def receive(event: T) -> None: ...



Rationale
=========

Although defining the variance of generic type by using type variables
that are constructed with the ``covariant=True`` or ``contravariant=True``
keyword arguments works well enough, it has downsides:

- In PEP 484 it states:

Covariance or contravariance is not a property of a type variable,
but a property of a generic class defined using this variable.

This is why type checkers do no allow using co- or contravariant type
variables outside of generic classes.
So, instead of defining variance on the type variable itself, it
would make more sense to mark the type variable's variance when
defining the generic type, and only there.

- Because variance is a property of the generic type itself, type
checkers do not allow using co- or contravariant type variables
outside of generic classes. But as the variance is currently a
property of the type variables, they must be used within generic
classes.

- Creating separate type variables for different variances often
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this is really common. It would be useful to have examples from open source projects where you see this duplication.

results in up to 3x code duplication, especially for type variables
with constraints or an upper bound.


Proposal
========


All of these issues can be solved by overloading the ``+`` and ``-``
prefix operators of ``typing.TypeVar`` instances, so that ``+T`` is an
alias for a covariant type variable, and ``-T`` for a contravariant
one. These variance operators should only be used within the parameter
list of ``typing.Generic`` and ``typing.Protocol`` when subclassing them.

This syntax is practically identical to type parameter variance in the
Scala programming language [1]_.



Specification
=============

The new type variable variance syntax should be used within the generic
parameter list of ``typing.Generic`` or ``typing.Protocol`` base classes,
and only there. It is not allowed to be used on type variables that
are already marked as covariant or contravariant, either with
``covariant=True``, ``contravariant=True`` or with the new ``+T`` or
``-T`` syntax.

Simplified Syntax
-----------------

Instead of specifying the variance on the typevar itself::

T_co = typing.TypeVar('T_co', covariant=True)
T_contra = typing.TypeVar('T_contra', contravariant=True)

class SupportsInvertOld(typing.Protocol[T_co]):
def __invert__(self) -> T_co: ...

class SupportsPartialOrderOld(typing.Protocol[T_contra]):
def __le__(self, other: T_contra): ...


The new syntax uses the ``+`` and ``-`` prefix operators to specify
covariant and contravariant generic types inline::

T = typing.TypeVar('T')

class SupportsInvert(typing.Protocol[+T]):
def __invert__(self) -> T: ...

class SupportsPartialOrder(typing.Protocol[-T]):
def __le__(self, other: T): ...


Differences with current syntax
-------------------------------

The new typevar operators return a transparent wrapper around the
original type variable, which can be accessed with the ``__origin__``
attribute on the returned wrapper. e.g.::

(+T).__origin__ is T
(+T).__covariant__ is True
(+T).__contravariant__ is False
(+T).__name__ == T.__name__
(+T).__constraints__ == T.__constraints__
(+T).__bound__ is T.__bound__


Thus, type variables defined with ``covariant=True`` and
``contravariant=True``, are not equivalent to ``+T`` and ``-T``.


``+T`` and ``-T`` are not valid type annotations, and should only be
used within the generic type parameter list of ``typing.Generic``
or ``typing.Protocol``. e.g.::

class Spam(typing.Generic[+KT]): ...
class Eggs(typing.Protocol[-KT, +VT]): ...
class HamSet(typing.Sequence[T], typing.Generic[+T]): ...

are valid uses.


All variance rules that apply to user-defined classes should apply
in the same way with the new syntax, as they do with the current syntax,
and vice-versa.



Rejected Ideas
==============

For more detauls about discussions, see links below:

- `Discussion in python/typing <https://github.com/python/typing/issues/813>`_

1. Using ``T_co = +TypeVar('T_co')`` instead of ``T_co = TypeVar('T_co', covariant=True)``
------------------------------------------------------------------------------------------

PROS:

- This requires minimal changes to the syntax
- Replaces the need to type ``covariant=True`` or ``contravariant=True``
with a concise operator.


CONS:

- The ``+`` and ``-`` copy the type variable, but type variables
should be unique.
- It is not obvious what to do with the name of the type variable.
- Co- and contravariance are properties of the generic class, not of
the individual type variables.


References
==========

.. [1] Scala Variance
https://docs.scala-lang.org/scala3/book/types-variance.html


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: