Skip to content

← Precise Type Hints with Literals and Generics step 4 of 26

Medium Primitives

Limit a Parameter to a Few Exact Values with Literal

You are writing the resize step of an image service. Callers choose a fit mode by name, and a typo such as "covr" should fail before the code runs. A Literal type does that. Literal["contain", "cover", "stretch"] allows only these three exact strings, so mypy rejects the typo:

resize((800, 600), (400, 400), "covr")
Argument 3 to "resize" has incompatible type "Literal['covr']"; expected "Literal['contain', 'cover', 'stretch']"  [arg-type]

The starter gives you a name for the type and a default:

from typing import Final, Literal

type Fit = Literal["contain", "cover", "stretch"]
DEFAULT_FIT: Final = "contain"

type Fit = ... creates a type alias, a name for a type, so you can write fit: Fit. Final means mypy rejects any later assignment to DEFAULT_FIT.

assert resize((800, 600), (400, 400)) == (400, 300)
assert resize((800, 600), (400, 400), "cover") == (533, 400)
assert resize((800, 600), (400, 400), "stretch") == (400, 400)
assert resize((1000, 1), (10, 10)) == (10, 1)
assert resize((2, 1), (5, 5)) == (5, 2)

What to write

def resize(
    image: tuple[int, int],
    target: tuple[int, int],
    fit: Fit = DEFAULT_FIT,
) -> tuple[int, int]:
    ...

image and target are (width, height) pairs of positive integers. Call the image size w, h and the target size tw, th.

Mode Result
contain Scale the image by min(tw / w, th / h), so it fits inside the target.
cover Scale the image by max(tw / w, th / h), so it fills the target and may stick out on one side.
stretch Return (tw, th).
  • Annotate fit as Fit, with DEFAULT_FIT as its default. Omitting fit means contain.
  • For contain and cover, compute each side as max(1, round(side * factor)), using / division.
  • Use one factor for both sides, and apply it to the original width and height. Do not work out one side from the other side's rounded result.
  • Return a tuple in (width, height) order. You may scale up as well as down.

Handle the modes in a match statement. It compares fit with each case in turn and runs the first one that matches:

match fit:
    case "contain":
        ...
    case "cover":
        ...
    case "stretch":
        ...
    case _:
        assert_never(fit)

case _ matches whatever is left. assert_never comes from typing, and it makes mypy report an error if any mode can reach that line. So a mode you forget to handle fails mypy. The next exercise explains how it works.

The diagram follows fit through the three comparisons. Choose else at each step, and no mode is left for case _. Choose then to follow one mode into its branch.

Loading visualization…

Edge cases

  • round rounds an exact half to the even integer, so round(2.5) is 2 and round(3.5) is 4. That is why resize((2, 1), (5, 5)) gives (5, 2) and not (5, 3).
  • A thin image keeps at least one pixel on each side, so resize((1000, 1), (10, 10)) gives (10, 1).

Keep the literal type

mypy gives a plain variable the wider type str. So this call fails, even though the value is valid:

mode = "cover"
resize((800, 600), (400, 400), mode)  # incompatible type "str"

Write mode: Fit = "cover", or pass the string directly. The default works the same way. DEFAULT_FIT: Final = "contain" keeps the type Literal["contain"]. DEFAULT_FIT: Final[str] = "contain" makes it a str, and mypy then rejects it as the default for fit.

Going further