Many languages like java
and php
share a concept of final
entities.
final
entity is something that can not be changed.
We did not have this feature in python
. Until two events happened recently:
- I have released
final-class
package -
python
core team has released officialfinal
support intyping
module
Now we truly have a new shiny language feature! Let's dig into how it works and why it's so awesome.
Declaring constants
First of all, you will need to install mypy
and type_extensions
:
» pip install mypy typing_extensions
Then we can start to use it:
from typing_extensions import Final
DAYS_IN_A_WEEK: Final = 7
That's it! But, what will happen if we try to modify this constant?
from typing_extensions import Final
DAYS_IN_A_WEEK: Final = 7
DAYS_IN_A_WEEK = 8 # I really want more days in a week!
Really, nothing. This is just good old python
where you can do bizarre things with no payback. It just does not care about type annotation.
All the magic happens only when we run mypy
type checker:
» mypy --python-version=3.6 --strict week.py
week.py:4: error: Cannot assign to final name "DAYS_IN_A_WEEK"
Boom! We have a constant here!
See how Final
type deals with underlying types. You don't have to manually tell the type checker what the type actually is. It will figure it out all by itself. In other words, type checker will know that DAYS_IN_A_WEEK
is int
.
Interfaces
And it goes beyond just declaring constants. You can declare your interface parts like attributes and methods that should not be changed:
from typing_extensions import Final, final
class BaseAPIDeclaration(object):
namespace: Final = 'api'
@final
def resolve(self) -> dict:
return {'namespace': self.namespace, 'base': True}
Now all subclasses of this imaginary class won't be able to redefine both namespace
and resolve()
. But, let's try to hack them to see what happens:
class ConcreteAPI(BaseAPIDeclaration):
namespace = 'custom-api'
def resolve(self) -> dict:
return {'hacking': True}
mypy
will back us up. Here's what the output will look like:
» mypy --python-version=3.6 --strict api.py
api.py:12: error: Cannot assign to final name "namespace"
api.py:14: error: Cannot override final attribute "resolve" (previously declared in base class "BaseAPIDeclaration")
Classes
And even classes can be final
. This way we can explicitly forbid to subclass classes not designed to be subclassed:
from typing_extensions import final
@final
class HRBusinessUnit(AbstractBusinessUnit):
def grant_permissions(self) -> None:
self.api.do_some_hr_stuff()
What does @final
decorator bring you? Confidence that nothing will break this contract:
class SubHRBusinessUnit(HRBusinessUnit):
def grant_permissions(self) -> None:
self.api.do_some_it_stuff()
This code will make mypy
quite unhappy (please, do not abuse robots!):
» mypy --python-version=3.6 --strict units.py
units.py:9: error: Cannot inherit from final class "HRBusinessUnit"
Now we can reason about why you should use it in your project.
Conclusion
Creating new restrictions is good for you: it makes your code cleaner, more readable, and increases its quality.
Strong points:
- it is clear from the definition what is a constant or a concrete realization and what is not
- our users will have strict API boundaries that can not be violated
- we can build closed systems that are not tolerant of breaking the rules
- it is easier to understand what happens inside your application
- it enforces composition over inheritance, which is a well-known best practice
Weak points: none! Write a comment if you can find any disadvantages.
Use types, create nice APIs, keep hacking!
Top comments (10)
I don't know how I feel about all of this. I see the value in helping tools with type declarations and mypy seems cool especially for large code bases. I'm also going to see if I can adopt optional static typing in a future project.
I'm not sure about trying to turn Python into a "closed" language is the answer though. Final classes in Java are one of ugliest thing they have, because if the API you create has an error (which can happen) all you're doing with this is make the developer's life harder (or in the case of Python just telling them to bypass mypy I guess).
One of the tenents of Python is its freedom and conventions. I can definitely tell you that in all these years the times I had to hack badly written "contracts" or APIs are more than you think. What if I couldn't because the library developer didn't trust his fellow developer enough to leave them the ability to "upgrade" the quality of the provided code? Even trust them to make a mess. If the code we wrote was perfect I would probably understand the need for final classes, but it's not.
Going over your conclusions:
for classes, you can use use the module abc to declare if a class or a method are abstract
Don't get this the wrong way but... who cares :-D
What I'm trying to say is that you can't anticipate avoiding any mistakes in API design and the web is full of people asking "why this Java method is private, please make it public because we need it" or "I just had to patch your Python class because this or that, fortunately I can do it, here's the diff to fix the problem".
"practicality beats purity", that's one of the lines in the Zen of Python.
If it works for you fine, but I'm not sure that these last two points are "pros" :-)
I've always had the feeling that Python has a silent contract with the developers saying: I trust you, you're an adult, you have the power to mess up, use it wisely.
this is always a good thing :)
Your points are perfectly valid. And I absolutely agree with you.
The thing I love about
python
is that you can still do whatever you want!Just add
# type: ignore
inline comment and yourmypy
issue will be gone.That's why we can have best of both worlds: closed systems that guide you and enough freedom to be an adult.
Thanks Nikita!
Some interesting conversations here about the general value (or detriment) of the type annotations.
I thought it was really interesting to read in the non-goals of PEP 484 (which introduced type annotations) that type annotations will never be required, "even by convention" (python.org/dev/peps/pep-0484/#non-...)
This says to me that the annotations are mostly geared towards large scale Python projects that probably should have been written in/should be rewritten in a statically typed language. It follows that smaller scale Python projects are meant to remain annotation free. Personally, I think this gives the best of both worlds in providing a way for large scale projects to gain some stability without over complicating the language.
Interesting to see how type annotations can be used to implement custom static type checks! Thanks for this.
What's the reason that Python didn't allow
final
annotations in the first place? Constant variables have their place and can be very useful.Python does not have constants, using the all caps is just a convention.
Thanks for sharing. I have been using the pyrite plugin for vscode for awhile now and really like the intuition it gives.
Excellent article, thank you!