239 lines
7.1 KiB
ReStructuredText
239 lines
7.1 KiB
ReStructuredText
PEP: 613
|
||
Title: Explicit Type Aliases
|
||
Author: Shannon Zhu <szhu@fb.com>
|
||
Sponsor: Guido van Rossum <guido@python.org>
|
||
Discussions-To: https://mail.python.org/archives/list/typing-sig@python.org/thread/MWRJOBEEEMFVXE7CAKO7B4P46IPM4AN3/
|
||
Status: Accepted
|
||
Type: Standards Track
|
||
Topic: Typing
|
||
Content-Type: text/x-rst
|
||
Created: 21-Jan-2020
|
||
Python-Version: 3.10
|
||
Post-History: 21-Jan-2020
|
||
|
||
|
||
Abstract
|
||
========
|
||
|
||
Type aliases are user-specified types which may be as complex as any type hint,
|
||
and are specified with a simple variable assignment on a module top level.
|
||
|
||
This PEP formalizes a way to explicitly declare an assignment as a type alias.
|
||
|
||
Motivation
|
||
==========
|
||
|
||
Type aliases are declared as top level variable assignments.
|
||
In :pep:`PEP 484 <484#type-aliases>`,
|
||
the distinction between a valid type alias and a global variable was implicitly
|
||
determined: if a top level assignment is unannotated, and the assigned value is
|
||
a valid type, then the name being assigned to is a valid type alias. Otherwise,
|
||
that name is simply a global value that cannot be used as a type hint.
|
||
|
||
These implicit type alias declaration rules create confusion when type aliases
|
||
involve forward references, invalid types, or violate other restrictions
|
||
enforced on type alias declaration. Because the distinction between an
|
||
unannotated value and a type alias is implicit, ambiguous or incorrect type
|
||
alias declarations implicitly default to a valid value assignment. This creates
|
||
expressions that are impossible to express as type aliases and punts error
|
||
diagnosis of malformed type aliases downstream.
|
||
|
||
The following examples each include an illustration of some of the suboptimal
|
||
or confusing behaviors resulting from existing implicit alias declarations.
|
||
We also introduce explicit aliases of the format ``TypeName: TypeAlias = Expression``
|
||
here for the sake of comparison, but the syntax is discussed in further detail
|
||
in later sections.
|
||
|
||
Forward References:
|
||
*******************
|
||
|
||
::
|
||
|
||
MyType = "ClassName"
|
||
def foo() -> MyType: ...
|
||
|
||
This code snippet should not error so long as ``ClassName`` is defined
|
||
later on. However, a type checker is forced to assume that MyType is a value
|
||
assignment rather than a type alias, and therefore may throw spurious errors
|
||
that (1) ``MyType`` is an unannotated global string, and (2) ``MyType``
|
||
cannot be used as a return annotation because it is not a valid type.
|
||
|
||
::
|
||
|
||
MyType: TypeAlias = "ClassName"
|
||
def foo() -> MyType: ...
|
||
|
||
Explicit aliases remove ambiguity so neither of the above errors will be
|
||
thrown. Additionally, if something is wrong with ``ClassName``
|
||
(i.e., it’s not actually defined later), the type checker can throw an error.
|
||
|
||
|
||
Error Messaging:
|
||
****************
|
||
|
||
::
|
||
|
||
MyType1 = InvalidType
|
||
MyType2 = MyGeneric(int) # i.e., intention was MyGeneric[int]
|
||
|
||
A type checker should warn on this code snippet that ``InvalidType`` is not
|
||
a valid type, and therefore cannot be used to annotate an expression or to
|
||
construct a type alias. Instead, type checkers are forced to throw spurious
|
||
errors that (1) ``MyType`` is a global expression missing an annotation,
|
||
and (2) ``MyType`` is not a valid type in all usages of ``MyType``
|
||
across the codebase.
|
||
|
||
::
|
||
|
||
MyType1: TypeAlias = InvalidType
|
||
MyType2: TypeAlias = MyGeneric(int)
|
||
|
||
With explicit aliases, the type checker has enough information to error on the
|
||
actual definition of the bad type alias, and explain why: that ``MyGeneric(int)``
|
||
and ``InvalidType`` are not valid types. When the value expression is no longer
|
||
evaluated as a global value, unactionable type errors on all usages of ``MyType``
|
||
across the codebase can be suppressed.
|
||
|
||
Scope Restrictions:
|
||
*******************
|
||
|
||
::
|
||
|
||
class Foo:
|
||
x = ClassName
|
||
y: TypeAlias = ClassName
|
||
z: Type[ClassName] = ClassName
|
||
|
||
Type aliases are valid within class scope, both implicitly (``x``) and
|
||
explicitly (``y``). If the line should be interpreted as a class
|
||
variable, it must be explicitly annotated (``z``).
|
||
|
||
::
|
||
|
||
x = ClassName
|
||
def foo() -> None:
|
||
x = ClassName
|
||
|
||
The outer ``x`` is a valid type alias, but type checkers must error if the
|
||
inner ``x`` is ever used as a type because type aliases cannot be defined
|
||
inside of a function.
|
||
This is confusing because the alias declaration rule is not explicit, and because
|
||
a type error will not be thrown on the location of the inner type alias declaration
|
||
but rather on every one of its subsequent use cases.
|
||
|
||
::
|
||
|
||
x: TypeAlias = ClassName
|
||
def foo() -> None:
|
||
x = ClassName
|
||
def bar() -> None:
|
||
x: TypeAlias = ClassName
|
||
|
||
With explicit aliases, the outer assignment is still a valid type variable.
|
||
Inside ``foo``, the inner assignment should be interpreted as ``x: Type[ClassName]``.
|
||
Inside ``bar``, the type checker should raise a clear error, communicating
|
||
to the author that type aliases cannot be defined inside a function.
|
||
|
||
|
||
Specification
|
||
=============
|
||
|
||
The explicit alias declaration syntax clearly differentiates between the three
|
||
possible kinds of assignments: typed global expressions, untyped global
|
||
expressions, and type aliases. This avoids the existence of assignments that
|
||
break type checking when an annotation is added, and avoids classifying the
|
||
nature of the assignment based on the type of the value.
|
||
|
||
Implicit syntax (pre-existing):
|
||
|
||
::
|
||
|
||
x = 1 # untyped global expression
|
||
x: int = 1 # typed global expression
|
||
|
||
x = int # type alias
|
||
x: Type[int] = int # typed global expression
|
||
|
||
|
||
Explicit syntax:
|
||
|
||
::
|
||
|
||
x = 1 # untyped global expression
|
||
x: int = 1 # typed global expression
|
||
|
||
x = int # untyped global expression (see note below)
|
||
x: Type[int] = int # typed global expression
|
||
|
||
x: TypeAlias = int # type alias
|
||
x: TypeAlias = "MyClass" # type alias
|
||
|
||
|
||
Note: The examples above illustrate implicit and explicit alias declarations in
|
||
isolation. For the sake of backwards compatibility, type checkers should support
|
||
both simultaneously, meaning an untyped global expression ``x = int`` will
|
||
still be considered a valid type alias.
|
||
|
||
|
||
Backwards Compatibility
|
||
=======================
|
||
|
||
Explicit aliases provide an alternative way to declare type aliases, but all
|
||
pre-existing code and old alias declarations will work as before.
|
||
|
||
|
||
Reference Implementation
|
||
========================
|
||
|
||
The `Pyre <https://pyre-check.org/>`_ type checker supports explicit type
|
||
alias declarations.
|
||
|
||
|
||
Rejected Ideas
|
||
==============
|
||
|
||
Some alternative syntaxes were considered for explicit aliases:
|
||
|
||
::
|
||
|
||
MyType: TypeAlias[int]
|
||
|
||
This looks a lot like an uninitialized variable.
|
||
|
||
::
|
||
|
||
MyType = TypeAlias[int]
|
||
|
||
Along with the option above, this format potentially adds confusion around
|
||
what the runtime value of ``MyType`` is.
|
||
|
||
|
||
In comparison, the chosen syntax option ``MyType: TypeAlias = int`` is
|
||
appealing because it still sticks with the ``MyType = int`` assignment
|
||
syntax, and adds some information for the type checker purely as an annotation.
|
||
|
||
|
||
Version History
|
||
===============
|
||
|
||
* 2021-11-16
|
||
|
||
* Allow TypeAlias inside class scope
|
||
|
||
|
||
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:
|