Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft documentation for final attributes #5754

Merged
merged 1 commit into from
Oct 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
267 changes: 267 additions & 0 deletions docs/source/final_attrs.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
Final names, methods and classes
================================
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would call out somewhat early in this section that there's both Final (a type qualifier or pseudo-type) and final (a decorator).


You can declare a variable or attribute as final, which means that the variable
must not be assigned a new value after initialization. This is often useful for
module and class level constants as a way to prevent unintended modification.
Mypy will prevent further assignments to final names in type-checked code:

.. code-block:: python

from typing_extensions import Final

RATE: Final = 3000
class Base:
DEFAULT_ID: Final = 0

# 1000 lines later

RATE = 300 # Error: can't assign to final attribute
Base.DEFAULT_ID = 1 # Error: can't override a final attribute

Another use case for final attributes is where a user wants to protect certain
instance attributes from overriding in a subclass:

.. code-block:: python

import uuid
from typing_extensions import Final

class Snowflake:
"""An absolutely unique object in the database"""
def __init__(self) -> None:
self.id: Final = uuid.uuid4()

# 1000 lines later
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment feels irrelevant.


class User(Snowflake):
id = uuid.uuid4() # Error: can't override a final attribute

Some other use cases might be solved by using ``@property``, but note that
neither of the above use cases can be solved with it.

.. note::

This is an experimental feature. Some details might change in later
versions of mypy. The final qualifiers are available in the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe instead of "the final qualifiers" write "the final decorator and Final pseudo-type"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed this with Jukka, and IIRC he suggested just "qualifiers".

``typing_extensions`` package available on PyPI.

Syntax variants
***************

The ``typing_extensions.Final`` qualifier indicates that a given name or
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I'm not sure that "qualifier" is the best term here, though I'm not sure that "pseudo-type" is any better. We probably should use the same word to describe "ClassVar" (see #5733).

attribute should never be re-assigned, re-defined, nor overridden. It can be
used in one of these forms:


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only one blank line.

* You can provide an explicit type using the syntax ``Final[<type>]``. Example:

.. code-block:: python

ID: Final[float] = 1

* You can omit the type: ``ID: Final = 1``. Note that unlike for generic
classes this is *not* the same as ``Final[Any]``. Here mypy will infer
type ``int``.

* In stub files you can omit the right hand side and just write
``ID: Final[float]``.

* Finally, you can define ``self.id: Final = 1`` (also with a type argument),
but this is allowed *only* in ``__init__`` methods.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the limitation to __init__ methods? In general we allow inferring or declaring the type of instance variables in any method (though IIRC a declaration must be on the first use). Is this because the implementation cannot ensure that there aren't any assignments in other places if Final isn't set in __init__?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the instance attribute should be set only once, when the instance is created. I think we should also allow this in __new__, but this is blocked by #1021. I will try to reword here to make this clear and add a follow-up issue for __new__.


Definition rules
****************

The are two rules that should be always followed when defining a final name:

* There can be *at most one* final declaration per module or class for
a given attribute:

.. code-block:: python

from typing_extensions import Final

ID: Final = 1
ID: Final = 2 # Error: "ID" already declared as final

class SomeCls:
id: Final = 1
def __init__(self, x: int) -> None:
self.id: Final = x # Error: "id" already declared in class body

Note that mypy has a single namespace for a class. So there can't be two
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop "two".

class-level and instance-level constants with the same name.

* There must be *exactly one* assignment to a final attribute:

.. code-block:: python

ID = 1
ID: Final = 2 # Error!

class SomeCls:
ID = 1
ID: Final = 2 # Error!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use a different name instead of ID for the class variable? Having the name the same distract from the point this example is making regarding duplication. I don't think you need an example showing that a final module global variable and a final class variable don't clash -- it's standard Python semantics (and common knowledge) that those namespaces don't interact.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I will update these.


* A final attribute declared in class body without an initializer must
be initialized in the ``__init__`` method (you can skip the initializer
in stub files):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, why limit this to __init__?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, why limit this to __init__?

Same as above.


.. code-block:: python

class SomeCls:
x: Final[int]
y: Final[int] # Error: final attribute without an initializer
def __init__(self) -> None:
self.x = 1 # Good

* ``Final`` can be only used as an 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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also very similar to ClassVar, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.


.. code-block:: python

x: List[Final[int]] = [] # Error!
def fun(x: Final[List[int]]) -> None: # Error!
...

* ``Final`` and ``ClassVar`` should not be used together. Mypy will infer
the scope of a final declaration automatically depending on whether it was
initialized in the class body or in ``__init__``.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, interesting. I can understand you don't want ClassVar[Final] or Final[ClassVar] (though the latter kind of sounds all right :-).


Using final attributes
**********************

As a result of a final declaration mypy strives to provide the
two following guarantees:

* A final attribute can't be re-assigned (or otherwise re-defined), both
internally and externally:

.. code-block:: python

# file mod.py
from typing_extensions import Final

ID: Final = 1

class SomeCls:
ID: Final = 1

def meth(self) -> None:
self.ID = 2 # Error: can't assign to final attribute
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again I think it would be less confusing if the module variable and the class variable had different names.


# file main.py
import mod
mod.ID = 2 # Error: can't assign to constant.

from mod import ID
ID = 2 # Also an error, see note below.

class DerivedCls(mod.SomeCls):
...

DerivedCls.ID = 2 # Error!
obj: DerivedCls
obj.ID = 2 # Error!

* A final attribute can't be overridden by a subclass (even with another
explicit final declaration). Note however, that final attributes can
override read-only properties. This also applies to multiple inheritance:

.. code-block:: python

class Base:
@property
def ID(self) -> int: ...

class One(Base):
ID: Final = 1 # OK

class Other(Base):
ID: Final = 2 # OK

class Combo(One, Other): # Error: cannot override final attribute.
pass

* Declaring a name as final only guarantees that the name wll not be re-bound
to other value, it doesn't make the value immutable. One can use immutable ABCs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other -> another

and containers to prevent mutating such values:

.. code-block:: python

x: Final = ['a', 'b']
x.append('c') # OK

y: Final[Sequance[str]] = ['a', 'b']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo (Sequence)

y.append('x') # Error: Sequance is immutable
z: Final = ('a', 'b') # Also an option

Final methods
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ironic that here "Final" is capitalized even though in the code it uses a lower-case 'f', whereas in previous section headings "final" was not capitalized but the code used Final. :-) Maybe add "(the @final decorator)" to the title here?

*************

Like with attributes, sometimes it is useful to protect a method from
overriding. In such situations one can use the ``typing_extensions.final``
decorator:

.. code-block:: python

from typing_extensions import final
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment emphasizing this uses a lower-case 'f'.


class Base:
@final
def common_name(self) -> None:
...

# 1000 lines later
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop.


class Derived(Base):
def common_name(self) -> None: # Error: cannot override a final method
...

This ``@final`` decorator can be used with instance methods, class methods,
static methods, and properties (this includes overloaded methods). For
overloaded methods one should add ``@final`` on the implementation to make
it final (or on the first overload in stubs):

.. code-block:: python
from typing import Any, overload

class Base:
@overload
def meth(self) -> None: ...
@overload
def meth(self, arg: int) -> int: ...
@final
def meth(self, x=None):
...

Final classes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add "(the @final class decorator)"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure this is needed, we can add this later.

*************

You can apply a ``typing_extensions.final`` decorator to a class to indicate
to mypy that it can't be subclassed. The decorator acts as a declaration
for mypy (and as documentation for humans), but it doesn't prevent subclassing
at runtime:

.. code-block:: python

from typing_extensions import final

@final
class Leaf:
...

class MyLeaf(Leaf): # Error: Leaf can't be subclassed
...

Here are some situations where using a final class may be useful:

* A class wasn't designed to be subclassed. Perhaps subclassing does not
work as expected, or it's error-prone.
* You want to retain the freedom to arbitrarily change the class implementation
in the future, and these changes might break subclasses.
* You believe that subclassing would make code harder to understand or maintain.
For example, you may want to prevent unnecessarily tight coupling between
base classes and subclasses.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These bullets all make almost the same point. Maybe we don't need to editorialize and users can figure out for themselves why final classes might be useful (they might even know the concept from another language).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, Jukka proposed to add some examples, and I think it probably makes sense to keep at least some of them.

1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Mypy is a static type checker for Python 3 and Python 2.7.
stubs
generics
more_types
final_attrs
metaclasses

.. toctree::
Expand Down