Skip to content

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

Easy Primitives

Check a Value Before You Use It

Your app reads its settings from a file. Some settings can be missing, and some come in two forms. Before your code uses a setting, it has to check which kind of value it has. mypy rejects code that skips the check.

Here is the first function you will write, done the quick way:

def attempts(retries: int | None) -> int:
    return retries + 1

retries is a number or None. mypy rejects the addition, because None + 1 would crash:

L2:12 [operator]
Unsupported operand types for + ("None" and "int")

Check for None first, and return early:

def attempts(retries: int | None) -> int:
    if retries is None:
        return 1
    return retries + 1

After the if, mypy knows that retries is an int. A check that rules out a type is called narrowing. You met it in the getting-started track. This exercise uses the three forms you will write most often.

What to write

  • attempts(retries) returns how many times to try a request. That is the first try plus retries more. A missing setting (None) means no retries, so it returns 1.
  • host_list(hosts) returns the hosts as a list. A setting can name one host, such as "db1", or a list of hosts. One host becomes a list of one, ["db1"]. A list comes back with the same hosts in the same order.
  • require(value, name) returns value when it is a string. When it is None, it raises ValueError(f"missing setting: {name}").

Keep solve as it is.

Use isinstance on a union

isinstance(hosts, str) narrows str | list[str]. Inside the if, hosts is a str. Below an if that returns, it is a list[str].

Watch for one bug that mypy does not catch. list(hosts) passes mypy, because a string is a sequence of characters. It turns "db1" into ["d", "b", "1"]. The visible tests catch it.

Raise when a value is required

In require, raise inside if value is None:. After that if, mypy knows that value is a str, so return value passes.

Edge cases

  • A retries of 0 gives 1 attempt.
  • An empty list of hosts gives [].
  • An empty string is still a string, so require("", "database_url") returns "". Only None is missing.

What solve returns

solve calls the three functions and returns a dictionary:

  • "attempts": the result of attempts.
  • "hosts": the result of host_list.
  • "database_url": the result of require for the database URL, or the error message, such as "missing setting: database_url", when it raises.