The ‘Literal’ Annotation in Python

· by

Contents
import pandas as pd
from typing import Literal


def normalize(
    df: pd.DataFrame, imputation: Literal[None, "fill0", "fillmean"]
): ...


with open("foo.txt", "q") as fp:
    fp.write("data")

Python keeps developing via Python Enhancement Proposals (PEPs). PEP 586 added a feature I love: Literal Types 😍

After reading this article you will know how to use that feature.

If you happen to be new to type annotations, I recommend reading my introduction to type annotations in Python.

Literal Annotation Example

The Literal type needs at least one parameter, e.g. Literal["foo"]. That means that the annotated variable can only take the string "foo". If you want to accept "bar" as well, you need to write Literal["foo", "bar"].

A complete example:

from typing import Literal
# If you are on Python 3.7 and upgrading is not an option,
#   $ pip install typing_extensions
# to install the backport. Then import it:
#   from typing_extensions import Literal


def parse_bool_str(
    text: str, default: Literal["raise", False] = "raise"
) -> bool:
    if text.lower() in ["y", "yes", "true", "1", "on"]:
        return True
    if text.lower() in ["n", "no", "false", "0", "off"]:
        return False
    if default == "raise":
        raise ValueError(f"It's unclear: {text}")
    return False

We could also have annotated default with Union[str, bool], but that would allow a big range of other values and different behavior.

Please note that you need to run mypy during development (best in your CI pipeline) to find such issues. It will NOT help you during runtime. Read my introduction to type annotations in Python for more about that.

Enum in Python

Instead of using a Literal type, you could use an enum:

from enum import Enum


class DefaultBehaviour(Enum):
    RAISE = "raise"
    BOOL = False


def parse_bool_str(
    text: str, default: DefaultBehaviour = DefaultBehaviour.RAISE
) -> bool:
    if text.lower() in ["y", "yes", "true", "1", "on"]:
        return True
    if text.lower() in ["n", "no", "false", "0", "off"]:
        return False
    if default == DefaultBehaviour.RAISE:
        raise ValueError(f"It's unclear: {text}")
    return False

You can see that this changes the code in multiple places. For this reason, it might be harder to use Enums for type annotations in an existing codebase.

Pydantic and Literal

When it comes to (de)serialization and data validation, I recently fell in love with pydantic 😍

Let’s test how pydantic deals with Literals:

from typing import Literal, List
from pydantic import BaseModel, Field


class User(BaseModel):
    name: str
    roles: List[Literal["Registered", "Moderator", "Admin"]] = Field(
        default_factory=list
    )


jon_doe = User.parse_obj({"name": "Jon Doe", "roles": []})
jon_skeet = User.parse_obj(
    {"name": "Jon Skeet", "roles": ["Registered", "Admin"]}
)
print(jon_skeet.dict())

# This fails - as expected!
# unexpected value; permitted: 'Registered', 'Moderator', 'Admin'
# (type=value_error.const;
#  given=foo;
#  permitted=('Registered', 'Moderator', 'Admin')
# )
User.parse_obj({"name": "Jon Doe", "roles": ["foo"]})

It works like a charm 🎉

What should I use in new code?

One important part of type annotations is editor support. You want the editor to be able to autocomplete as much as possible. For VS Code + Pylance, I can say that Enums give better autocompletion:

from typing import Literal, List
from dataclasses import dataclass
from enum import Enum


class EnumRoles(Enum):
    REGISTERED = "Registered"
    MODERATOR = "Moderator"
    ADMIN = "ADMIN"


LiteralRoles = Literal["Registered", "Moderator", "Admin"]


@dataclass
class User:
    name: str
    role_literal: LiteralRoles
    role_enum: EnumRoles


User(name="foo", role_literal="Moderator", role_enum=EnumRoles.MODERATOR)

In principle, both allow the developer to see the list of allowed values. The editor support could be very similar — but it isn’t. Hence I would prefer Enums for new code. It’s not a strong preference, though.

Here is a similar discussion for TypeScript: Difference between string enums and string literal types in TS