sobolevn's personal blog

1-minute guide to real constants in Python

4 mins read Start a discussion Edit this page

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

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:

  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!


Subscribe to my blog if you want more

Republished as: