Skip to content

PEP draft for +T and -T type variable syntax #2066

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 15 commits into from
Closed
Changes from all commits
Commits
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
252 changes: 252 additions & 0 deletions pep-9999.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
PEP: 9999
Title: Inline variance operators for generic type variables: ``+T`` and ``-T``
Author: Joren Hammudoglu <jhammudoglu at gmail.com>
Sponsor: Jelle Zijlstra <jelle.zijlstra at gmail.com>
Status: Draft
Type: Standards Track
Content-Type: text/x-rst
Created: 24-Jul-2021
Post-History: 24-Jul-2021


Abstract
========

This PEP proposes a consise syntax for specifying the variance of
the type parameters of `user-defined generic types
<https://www.python.org/dev/peps/pep-0484/#user-defined-generic-types>`_.
This allow declaring a co- or contravariant type parameter with ``+T`` or
``-T``, where ``T`` is a type variable.


Terminology
===========

This PEP uses some terms to refer to specific concepts related to Python
typing, or type systems in general. For the sake of clarity, this PEP
considers the following terms and definitions:

type variable
An instance of ``typing.TypeVar`` or ``typing.ParamSpec``, as defined
in `PEP 484 <https://www.python.org/dev/peps/pep-0484/>`_ and
PEP `PEP 612 <https://www.python.org/dev/peps/pep-0612/>`_. Type
variables can be used as either type annotations, or to declare
user-defined generic types.

type parameter
Generic types can have one or multiple type parameters. For user-defined
generic types, these are declared using type variables. Unlike type variables,
type parameters only occur in generic types.

generic type
A generic type, or generic class, is a class with a base class
``typing.Generic``, ``typing.Protocol``, or another generic type,
and has at least one type parameter.

variance
A type parameter of a generic type can be either covariant, contravariant,
or invariant (the default). This defines the subtyping relation of the
generic parameters and the generic type. See `PEP 484
<https://www.python.org/dev/peps/pep-0484/#covariance-and-contravariance>`_.

variadic
A variadic type parameter is either co- or contravariant. A variadic
type (or variadic class) is a generic type with at least one variadic
type parameter


Rationale
=========

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.

More specifically, variance is a property of the *type parameters* of a
generic class.

Currently, variadic type parameters are declared with type varianbles

Choose a reason for hiding this comment

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

s/varianbles/variables/

that are constructed with ``covariant=True`` or ``contravariant=True``,
e.g.:
::

from typing import TypeVar, Generic, Protocol

T_co = TypeVar("T_co", covariant=True)
T_contra = TypeVar("T_contra", contravariant=True)

class SimpleFunction(Protocol[T_contra, T_co]):
def __call__(self, x: T_contra) -> T_co: ...

But type variables can also be used outside of the scope of generic
types, where the concept of variance does not apply, e.g.:
::

def first(things: Iterable[T]) -> T:
return next(iter(things))


This example is valid, but using ``T_co`` or ``T_contra`` instead of
``T`` here does not make sense, and type checkers do not allow is.

In ``SimpleFunction``, ``T_co`` or ``T_contra`` are used to annotate
its method as well. This is required because the variance is associated
with the individual type variables. But the method signature is unrelated
to the variance of the type parameters of its generic type,
``SimpleFunction`` in this example.

Because variance is restricted to the type parameters of generic types,
it makes more sense to specify the variance of type parameters directly
where the generic type is defined, removing the need to specify it on
the type variables themselves.


Proposal
========

The variance of the type parameters of a generic class can be specified
with the ``+`` and ``-`` prefix operators on their respective type
variables within the type parameter list of ``typing.Generic``,
``typing.Protocol``, or the generic base class. Instead of
::

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
variadic type parameters in the same place where the generic type is
declared:
::

T = TypeVar("T")

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

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

This syntax is inspired by Scala programming language [1]_.



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


Valid use locations
-------------------


The new type variable variance syntax can only be used within the type
parameter list of ``typing.Generic``, ``typing.Protocol``, or a generic
base class.

The ``+`` and ``-`` prefix operators can be used on ``typing.TypeVar``
and ``typing.ParamSpec`` instances, that are now already variadic, i.e.
its ``__covariant__`` and ``__contravariant__`` attributes must be
``False``.

When the same type variable is used on multiple generic base classes,
they must share the same variance, e.g.
::

from typing import TypeVar, Callable, Container, Iterable, Protocol

class LinkedList(Iterable[+T], Container[+T]): ...
class EventListener(Callable[[-T], None], Protocol[-T]): ...

are valid examples.


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 generic base classes, e.g.::

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

are valid uses.

All variance rules that apply to user-defined generic types 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: