PEP 613 – Explicit Type Aliases
- Author:
- Shannon Zhu <szhu at fb.com>
- Sponsor:
- Guido van Rossum <guido at python.org>
- Discussions-To:
- Typing-SIG thread
- Status:
- Accepted
- Type:
- Standards Track
- Topic:
- Typing
- 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 484, 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 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.
Source: https://github.com/python/peps/blob/main/pep-0613.rst
Last modified: 2022-10-07 00:36:39 GMT