Python Literal, New and Final Types

Python type annotations are an excellent way to communicate your intentions across your development teams (to enforce your intention, you need to use a type checker like mypy though).

This makes your code base more robust.

Multiple types are accessible in Python. Here we go into the detail of Literal, NewType and Final.

To illustrate the concepts, we will work with the following Cheese object:

from dataclasses import dataclass

@dataclass
class Cheese:
    """
    Class representing a cheese object.
    """

    name: str
    price_per_kilo: float
    aoc: bool


if __name__ == "__main__":

    cheese = Cheese(
        name="roquefort",
        price_per_kilo=25.75,
        aoc=True
    )

Note: AOC stands for Appellation d’Origine Controlée. As a trademark, this label intends to protect French Cheeses from counterfeiting. Some cheese are protected. Some are not. There are over 1,600 varieties of cheese. Each of them pairing well with specific wines.

Literal

The Literal type allows you to restrict the variable to a very specific set of values.

from dataclasses import dataclass
from typing import Literal

@dataclass
class Cheese:

    name: Literal["roquefort", "comté", "brie"]
    price_per_kilo: float
    aoc: bool


if __name__ == "__main__":

    cheese = Cheese(
        name="abondance",
        price_per_kilo=28,
        aoc=True
    )

If you run mypy on the command line against that file, you will get an error:

error: Argument "name" to "Cheese" has incompatible type "Literal['abondance']";
expected "Literal['roquefort', 'comté', 'brie']"  [arg-type]

Tip: In most development environments you can get the typechecker analysis in real time. In Visual Studio Code, you can get notified of errors as you type using the MyPy Type Checking extension: code --install-extension matangover.mypy.

Notes:

  • To restrict possible values of a variable, you can also use Python Enumerations. However, Literal is more lightweight.

  • You can also use Annotated types to specify more complex constraints (e.g. to constrain a string to a specific size or to match a regular expression) but this type is best served as a communication method.

NewType

A NewType takes an existing type and creates a brand new type that possesses the exact same fields and methods as the existing type.

NewType is useful in a handful of real-world scenario.

For instance, to make sure you only operate upon sanitized strings to prevent SQL injections you could establish a distinction between a str and a SanitizedString.

Same to separate between a User and a LoggedInUser.

Let’s say we are a gastronomic restaurant. We only want to serve Protected Cheese to our customers.

For the service, a dedicated method called dispense_protected_cheese_to_customer makes sure that
only protected cheese can be served. And for sure, as it only takes our new ProtectedCheese type (based on the Cheese type) as argument and refuses everything else.

from dataclasses import dataclass
from typing import NewType

@dataclass
class Cheese:

    name: str
    price_per_kilo: float
    aoc: bool


ProtectedCheese = NewType("ProtectedCheese", Cheese)


def prepare_for_serving(
    cheese: Cheese
) -> ProtectedCheese:
    protected_cheese = ProtectedCheese(cheese)
    # you can image other suitable methods being
    # applied here...
    return protected_cheese


def dispense_protected_cheese_to_customer(
    protected_cheese: ProtectedCheese
) -> None:
    # ...
    return None


if __name__ == "__main__":

    cheese = Cheese(
        name="charolais",
        price_per_kilo=47.60,
        aoc=True
    )

    protected_cheese = prepare_for_serving(cheese)
    dispense_protected_cheese_to_customer(protected_cheese)

You can try twisting around the above snippet yourself, by trying to serve a non-protected cheese to a customer:

protected_cheese = prepare_for_serving(cheese)
dispense_protected_cheese_to_customer(cheese)

Mypy will complain:

error: Argument 1 to "dispense_protected_cheese_to_customer" has incompatible type "Cheese";
expected "ProtectedCheese"  [arg-type]

Note: The prepare_for_serving method acts as a blessed function, creating our protected cheese from our original cheese blueprint type. It is important to note that the only way to create new types is through a set of blessed functions.

One more thing; NewType is a useful pattern to be aware of. However, classes and invariants provide a similar but much stronger guarantees to avoid illegal states.

Final Types

Final types allow you to prevent a type from changing its value over time (e.g. if you do not want the name of a variable to be changed by accident).

from typing import Final

RESTAURANT_NAME: Final = "Le Central"

Should you try set the variable to a new value, mypy will complain:

from typing import Final

RESTAURANT_NAME: Final = "Le Central"

if __name__ == "__main__":

    RESTAURANT_NAME = "Le Bois sans feuilles"
error: Cannot assign to final name "RESTAURANT_NAME"  [misc]

Final is hence useful to prevent a variable from being rebound.

Note: Both restaurants belong to the Troisgros family. Both are based in Roanne. Le Bois sans feuilles is a prestigious three-stars restaurant. A documentary about it was released in 2023: Menus Plaisirs – Les Troisgros.

Type Aliases and Variables Annotations

Even though it is cumbersome, you can add type annotations not only to methods but to variables:

def reverse_string(string_candidate: str) -> str:
    reversed_string: str = string_candidate[::-1]
    return reversed_string

You can also alias a particularly long type:

from typing import Dict, List

DictOfLists = Dict[str, List[int]]

def element_in_lists(
    dictionary: DictOfLists,
    element: int
) -> list[bool]:
    return [element in l for _, l in dictionary.items()]

if __name__ == "__main__":

    my_dict = {
        "a": [1,2],
        "b": [],
        "c": [42, 2, -1]
    }

    result = element_in_lists(
        dictionary=my_dict,
        element=42
    )
    print(result)

Acknowledgment

This post is based on the excellent book: Robust Python: Write Clean and Maintainable Code, Patrick Viafore, O’REILLY.

Leave a Reply

Your email address will not be published. Required fields are marked *