DEV Community

Cover image for 1-minute guide to real constants in Python
Nikita Sobolev for wemake.services

Posted on • Updated on

1-minute guide to real constants in Python

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:

  1. I have released final-class package
  2. python core team has released official final support in typing 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
Enter fullscreen mode Exit fullscreen mode

Then we can start to use it:

from typing_extensions import Final

DAYS_IN_A_WEEK: Final = 7
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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:

  1. it is clear from the definition what is a constant or a concrete realization and what is not
  2. our users will have strict API boundaries that can not be violated
  3. we can build closed systems that are not tolerant of breaking the rules
  4. it is easier to understand what happens inside your application
  5. 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)

Collapse
 
rhymes profile image
rhymes • Edited

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:

it is clear from the definition what is a constant or a concrete realization and what is not

for classes, you can use use the module abc to declare if a class or a method are abstract

our users will have strict API boundaries that can not be violated

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".

we can build closed systems that are not tolerant of breaking the rules

"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.

it enforces composition over inheritance, which is a well-known best practice

this is always a good thing :)

Collapse
 
sobolevn profile image
Nikita Sobolev

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 your mypy 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.

Collapse
 
rhymes profile image
rhymes

Thanks Nikita!

Collapse
 
nbelakovski profile image
nbelakovski

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.

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
florimondmanca profile image
Florimond Manca

Interesting to see how type annotations can be used to implement custom static type checks! Thanks for this.

Collapse
 
awwsmm profile image
Andrew (he/him)

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.

Collapse
 
rhymes profile image
rhymes

Python does not have constants, using the all caps is just a convention.

Collapse
 
waylonwalker profile image
Waylon Walker

Thanks for sharing. I have been using the pyrite plugin for vscode for awhile now and really like the intuition it gives.

Collapse
 
nbelakovski profile image
nbelakovski

Excellent article, thank you!