python-peps/pep-0591.rst

309 lines
9.5 KiB
ReStructuredText
Raw Normal View History

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
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 wasnt 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 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 revisisted 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
==========
.. [#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 586, 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: