PEP 613: Explicit Type Aliases (#1278)
This commit is contained in:
parent
00a0215285
commit
60d5d10a1d
|
@ -0,0 +1,223 @@
|
||||||
|
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: Draft
|
||||||
|
Type: Informational
|
||||||
|
Content-Type: text/x-rst
|
||||||
|
Created: 21-Jan-2020
|
||||||
|
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 484 <https://www.python.org/dev/peps/pep-0484/#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 :code:`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 :code:`ClassName` is defined
|
||||||
|
later on. However, a type checker is forced to throw spurious errors that
|
||||||
|
(1) :code:`MyType` is an unannotated global string, and (2) :code:`MyType`
|
||||||
|
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 :code:`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 :code:`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) :code:`MyType` is a global expression missing an annotation,
|
||||||
|
and (2) :code:`MyType` is not a valid type in all usages of :code:`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 :code:`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 :code:`MyType`
|
||||||
|
across the codebase can be suppressed.
|
||||||
|
|
||||||
|
Scope Restrictions:
|
||||||
|
*******************
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
x = ClassName
|
||||||
|
def foo() -> None:
|
||||||
|
x = ClassName
|
||||||
|
|
||||||
|
The outer :code:`x` is a valid type alias, but type checkers must error if the
|
||||||
|
inner :code:`x` is ever used as a type. This is confusing because the alias
|
||||||
|
declaration rule is not explicit, and the error is not 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,
|
||||||
|
and the inner assignment can either be a valid local variable or a clear error,
|
||||||
|
communicating to the author that type aliases cannot be defined inside a nested
|
||||||
|
scope.
|
||||||
|
|
||||||
|
|
||||||
|
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 :code:`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 :code:`MyType` is.
|
||||||
|
|
||||||
|
|
||||||
|
In comparison, the chosen syntax option :code:`MyType: TypeAlias = int` is
|
||||||
|
appealing because it still sticks with the :code:`MyType = int` assignment
|
||||||
|
syntax, and adds some information for the type checker purely as an annotation.
|
||||||
|
|
||||||
|
|
||||||
|
Open Issues
|
||||||
|
===========
|
||||||
|
|
||||||
|
|
||||||
|
The current implementation utilizes the :code:`TypeAlias` already defined in :code:`typing.pyi`,
|
||||||
|
but for conciseness there are suggestions to standardize this to a new class, :code:`typing.Alias`.
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
Loading…
Reference in New Issue