300 lines
9.1 KiB
ReStructuredText
300 lines
9.1 KiB
ReStructuredText
PEP: 591
|
||
Title: Adding a final qualifier to typing
|
||
Author: Michael J. Sullivan <sully@msully.net>, Ivan Levkivskyi <levkivskyi@gmail.com>
|
||
BDFL-Delegate: Guido van Rossum <guido@python.org>
|
||
Discussions-To: typing-sig@python.org
|
||
Status: Accepted
|
||
Type: Standards Track
|
||
Topic: Typing
|
||
Content-Type: text/x-rst
|
||
Created: 15-Mar-2019
|
||
Python-Version: 3.8
|
||
Post-History:
|
||
Resolution: https://mail.python.org/archives/list/typing-sig@python.org/message/FDO4KFYWYQEP3U2HVVBEBR3SXPHQSHYR/
|
||
|
||
|
||
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 field 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):
|
||
...
|
||
|
||
It is an error to use ``@final`` on a non-method function.
|
||
|
||
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 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
|
||
|
||
Note that a type checker need not allow ``Final`` declarations inside loops
|
||
since the runtime will see multiple assignments to the same variable in
|
||
subsequent iterations.
|
||
|
||
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 revisited in the future.
|
||
|
||
It would be possible to have the ``@final`` decorator on classes
|
||
dynamically prevent subclassing at runtime. Nothing else in ``typing``
|
||
does any runtime enforcement, though, so ``final`` will not either.
|
||
A workaround for when both runtime enforcement and static checking is
|
||
desired is to use this idiom (possibly in a support module)::
|
||
|
||
if typing.TYPE_CHECKING:
|
||
from typing import final
|
||
else:
|
||
from runtime_final import final
|
||
|
||
|
||
References
|
||
==========
|
||
|
||
.. [#mypy] http://www.mypy-lang.org/
|
||
|
||
.. [#typing_extensions] https://github.com/python/typing/tree/master/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:
|