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