PEP 591: Adding a final qualifier to typing (#990)
This commit is contained in:
parent
696664643d
commit
f3b44fbe4d
|
@ -0,0 +1,293 @@
|
|||
PEP: 591
|
||||
Title: Adding a final qualifier to typing
|
||||
Author: Michael J. Sullivan <sully@msully.net>, Ivan Levkivskyi <levkivskyi@gmail.com>
|
||||
Discussions-To: typing-sig@python.org
|
||||
Status: Draft
|
||||
Type: Standards Track
|
||||
Content-Type: text/x-rst
|
||||
Created: 15-Mar-2019
|
||||
Post-History:
|
||||
|
||||
|
||||
Abstract
|
||||
========
|
||||
|
||||
This PEP proposes a "final" qualifier to be added to the ``typing``
|
||||
module---in the form of a ``final`` decorator and a ``Final`` type
|
||||
annotation---to serve three related purposes:
|
||||
|
||||
* Declaring that a method should not be overridden
|
||||
* Declaring that a class should not be subclassed
|
||||
* Declaring that a variable or attribute should not be reassigned
|
||||
|
||||
|
||||
Motivation
|
||||
==========
|
||||
|
||||
The ``final`` decorator
|
||||
-----------------------
|
||||
The current ``typing`` module lacks a way to restrict the use of
|
||||
inheritance or overriding at a typechecker level. This is a common
|
||||
feature in other object-oriented languages (such as Java), and is
|
||||
useful for reducing the potential space of behaviors of a class,
|
||||
easing reasoning.
|
||||
|
||||
Some situations where a final class or method may be useful include:
|
||||
|
||||
* A class wasn’t designed to be subclassed or a method wasn't designed
|
||||
to be overridden. Perhaps it would not work as expected, or be
|
||||
error-prone.
|
||||
* Subclassing or overriding would make code harder to understand or
|
||||
maintain. For example, you may want to prevent unnecessarily tight
|
||||
coupling between base classes and subclasses.
|
||||
* You want to retain the freedom to arbitrarily change the class
|
||||
implementation in the future, and these changes might break
|
||||
subclasses.
|
||||
|
||||
The ``Final`` annotation
|
||||
------------------------
|
||||
|
||||
The current ``typing`` module lacks a way to indicate that a variable
|
||||
will not be assigned to. This is a useful feature in several
|
||||
situations:
|
||||
|
||||
* Preventing unintended modification of module and class level
|
||||
constants and documenting them as constants in a checkable way.
|
||||
* Creating a read-only attribute that may not be overridden by
|
||||
subclasses. (``@property`` can make an attribute read-only but
|
||||
does not prevent overriding)
|
||||
* Allowing a name to be used in situations where ordinarily a literal
|
||||
is expected (for example as a file name for ``NamedTuple``, a tuple
|
||||
of types passed to ``isinstance``, or an argument to a function
|
||||
with arguments of ``Literal`` type [#PEP-586]_).
|
||||
|
||||
Specification
|
||||
=============
|
||||
|
||||
The ``final`` decorator
|
||||
-----------------------
|
||||
|
||||
The ``typing.final`` decorator is used to restrict the use of
|
||||
inheritance and overriding.
|
||||
|
||||
A type checker should prohibit any class decorated with ``@final``
|
||||
from being subclassed and any method decorated with ``@final`` from
|
||||
being overridden in a subclass. The method decorator version may be
|
||||
used with all of instance methods, class methods, static methods, and properties.
|
||||
|
||||
For example::
|
||||
|
||||
from typing import final
|
||||
|
||||
@final
|
||||
class Base:
|
||||
...
|
||||
|
||||
class Derived(Base): # Error: Cannot inherit from final class "Base"
|
||||
...
|
||||
|
||||
and::
|
||||
|
||||
from typing import final
|
||||
|
||||
class Base:
|
||||
@final
|
||||
def foo(self) -> None:
|
||||
...
|
||||
|
||||
class Derived(Base):
|
||||
def foo(self) -> None: # Error: Cannot override final attribute "foo"
|
||||
# (previously declared in base class "Base")
|
||||
...
|
||||
|
||||
|
||||
For overloaded methods, ``@final`` should be placed on the
|
||||
implementation (or on the first overload, for stubs)::
|
||||
|
||||
from typing import Any, overload
|
||||
|
||||
class Base:
|
||||
@overload
|
||||
def method(self) -> None: ...
|
||||
@overload
|
||||
def method(self, arg: int) -> int: ...
|
||||
@final
|
||||
def method(self, x=None):
|
||||
...
|
||||
|
||||
The ``Final`` annotation
|
||||
------------------------
|
||||
|
||||
The ``typing.Final`` type qualifier is used to indicate that a
|
||||
variable or attribute should not be reassigned, redefined, or overridden.
|
||||
|
||||
Syntax
|
||||
~~~~~~
|
||||
|
||||
``Final`` may be used in in one of several forms:
|
||||
|
||||
* With an explicit type, using the syntax ``Final[<type>]``. Example::
|
||||
|
||||
ID: Final[float] = 1
|
||||
|
||||
* With no type annotation. Example::
|
||||
|
||||
ID: Final = 1
|
||||
|
||||
The typechecker should apply its usual type inference mechanisms to
|
||||
determine the type of ``ID`` (here, likely, ``int``). Note that unlike for
|
||||
generic classes this is *not* the same as ``Final[Any]``.
|
||||
|
||||
* In class bodies and stub files you can omit the right hand side and just write
|
||||
``ID: Final[float]``. If the right hand side is omitted, there must
|
||||
be an explicit type argument to ``Final``.
|
||||
|
||||
* Finally, as ``self.id: Final = 1`` (also optionally with a type in
|
||||
square brackets). This is allowed *only* in ``__init__`` methods, so
|
||||
that the final instance attribute is assigned only once when an
|
||||
instance is created.
|
||||
|
||||
|
||||
Semantics and examples
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The two main rules for defining a final name are:
|
||||
|
||||
* There can be *at most one* final declaration per module or class for
|
||||
a given attribute. There can't be separate class-level and instance-level
|
||||
constants with the same name.
|
||||
|
||||
* There must be *exactly one* assignment to a final name.
|
||||
|
||||
This means a type checker should prevent further assignments to final
|
||||
names in type-checked code::
|
||||
|
||||
from typing import Final
|
||||
|
||||
RATE: Final = 3000
|
||||
|
||||
class Base:
|
||||
DEFAULT_ID: Final = 0
|
||||
|
||||
RATE = 300 # Error: can't assign to final attribute
|
||||
Base.DEFAULT_ID = 1 # Error: can't override a final attribute
|
||||
|
||||
for x in [1, 2, 3]:
|
||||
FOO: Final = x # Error: Cannot use Final inside a loop
|
||||
|
||||
error: Cannot use Final inside a loop
|
||||
|
||||
Additionally, a type checker should prevent final attributes from
|
||||
being overridden in a subclass::
|
||||
|
||||
from typing import Final
|
||||
|
||||
class Window:
|
||||
BORDER_WIDTH: Final = 2.5
|
||||
...
|
||||
|
||||
class ListView(Window):
|
||||
BORDER_WIDTH = 3 # Error: can't override a final attribute
|
||||
|
||||
A final attribute declared in a class body without an initializer must
|
||||
be initialized in the ``__init__`` method (except in stub files)::
|
||||
|
||||
class ImmutablePoint:
|
||||
x: Final[int]
|
||||
y: Final[int] # Error: final attribute without an initializer
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.x = 1 # Good
|
||||
|
||||
Type checkers should infer a final attribute that is initialized in
|
||||
a class body as being a class variable. Variables should not be annotated
|
||||
with both ``ClassVar`` and ``Final``.
|
||||
|
||||
``Final`` may only be used as the outermost type in assignments or variable
|
||||
annotations. Using it in any other position is an error. In particular,
|
||||
``Final`` can't be used in annotations for function arguments::
|
||||
|
||||
x: List[Final[int]] = [] # Error!
|
||||
|
||||
def fun(x: Final[List[int]]) -> None: # Error!
|
||||
...
|
||||
|
||||
Note that declaring a name as final only guarantees that the name will
|
||||
not be re-bound to another value, but does not make the value
|
||||
immutable. Immutable ABCs and containers may be used in combination
|
||||
with ``Final`` to prevent mutating such values::
|
||||
|
||||
x: Final = ['a', 'b']
|
||||
x.append('c') # OK
|
||||
|
||||
y: Final[Sequence[str]] = ['a', 'b']
|
||||
y.append('x') # Error: "Sequence[str]" has no attribute "append"
|
||||
z: Final = ('a', 'b') # Also works
|
||||
|
||||
|
||||
Type checkers should treat uses of a final name that was initialized
|
||||
with a literal as if it was replaced by the literal. For example, the
|
||||
following should be allowed::
|
||||
|
||||
from typing import NamedTuple, Final
|
||||
|
||||
X: Final = "x"
|
||||
Y: Final = "y"
|
||||
N = NamedTuple("N", [(X, int), (Y, int)])
|
||||
|
||||
|
||||
Reference Implementation
|
||||
========================
|
||||
|
||||
The mypy [#mypy]_ type checker supports `Final` and `final`. A
|
||||
reference implementation of the runtime component is provided in the
|
||||
``typing_extensions`` [#typing_extensions]_ module.
|
||||
|
||||
|
||||
Rejected/deferred Ideas
|
||||
=======================
|
||||
|
||||
The name ``Const`` was also considered as the name for the ``Final``
|
||||
type annotation. The name ``Final`` was chosen instead because the
|
||||
concepts are related and it seemed best to be consistent between them.
|
||||
|
||||
We considered using a single name ``Final`` instead of introducing
|
||||
``final`` as well, but ``@Final`` just looked too weird to us.
|
||||
|
||||
A related feature to final classes would be Scala-style sealed
|
||||
classes, where a class is allowed to be inherited only by classes
|
||||
defined in the same module. Sealed classes seem most useful in
|
||||
combination with pattern matching, so it does not seem to justify the
|
||||
complexity in our case. This could be revisisted in the future.
|
||||
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
.. [#PEP-484] PEP 484, Type Hints, van Rossum, Lehtosalo, Langa
|
||||
(http://www.python.org/dev/peps/pep-0484)
|
||||
|
||||
.. [#PEP-526] PEP 526, Syntax for Variable Annotations, Gonzalez,
|
||||
House, Levkivskyi, Roach, van Rossum
|
||||
(http://www.python.org/dev/peps/pep-0526)
|
||||
|
||||
.. [#PEP-586] PEP 486, Literal Types, Lee, Levkivskyi, Lehtosalo
|
||||
(http://www.python.org/dev/peps/pep-0586)
|
||||
|
||||
.. [#mypy] http://www.mypy-lang.org/
|
||||
|
||||
.. [#typing_extensions] https://github.com/python/typing/typing_extensions
|
||||
|
||||
Copyright
|
||||
=========
|
||||
|
||||
This document has been placed in the public domain.
|
||||
|
||||
..
|
||||
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