Abstract types

You already encountered the basic syntax. Let’s focus here on more advanced concepts. Until now, only basic built-in types have been used: int, float, list[...], dict[..., ...], etc. The only more abstract type which has been used has been collections.abc.Callable. This type declares that the parameter supports a specific behavior: being called (potentially with input and output arguments). Several types allow to be called. As long as the type behaves as intended, it is all that matter. Only relying on the behavior is called duck typing at runtime and structural sub-typing when applied statically (before running).

collections.abc contains the built-in types. If several potential types might be expected, use |.

Note

Use abstract types for input arguments, however, use concrete types for output arguments if possible: you only want to enforce a behavior for inputs, however, you usually know what you are returning.

Exercise (easy)

Complete the type hints. Write some example calls to see the type checker in action.

1from collections.abc import ...
2
3def grade_exam(question_grades: ...) -> ...:
4    """question_grades stores the grade for every question"""
5    return sum(question_grades)
Correction
1from collections.abc import Iterable
2
3def grade_exam(question_grades: Iterable[int|float]) -> int|float:
4    """question_grades stores the grade for every question"""
5    return sum(question_grades)
6
7grade: float = grade_exam((1, 1.5))
8grade_: int = grade_exam((1, 1.5))  # not ok
9grade_exam((1, "1.5"))  # not ok

Exercise (intermediate)

Complete the type hints. Pay attention to the brackets in the return statement. Write some example calls to see the type checker in action.

 1from collections.abc import ...
 2
 3def select_adult_students(student_ages: ...) -> ...:
 4    """student_ages stores key(name):value(age) pairs"""
 5    return (name for name, age in student_ages.items() if age > 18)
 6
 7def select_adult_students_(student_ages: ...) -> ...:
 8    """student_ages stores key(name):value(age) pairs"""
 9    for name, age in student_ages.items():
10        if age > 18:
11            yield name
Correction
1from collections.abc import Mapping, Generator
2
3def select_adult_students(student_ages: Mapping[str, int]) -> Generator[str]:
4    return (name for name, age in student_ages.items() if age > 18)
5
6adults: tuple[str, ...] = tuple(select_adult_students({"a": 17}))
7adults_: list[str] = select_adult_students({"a": 17})  # not ok

Exercise (intermediate)

Complete the type hints.

 1from collections.abc import Callable, Sequence
 2
 3def admit_student(
 4    one_student_names: ..., # every names of a student in order
 5    has_paid_fees: ...,
 6    generate_unique_email: ...,
 7) -> ...:
 8    if has_paid_fees(one_student_names):
 9        return generate_unique_email(one_student_names)
10    return None
Correction
 1from collections.abc import Callable, Sequence
 2
 3def admit_student(
 4    one_student_names: Sequence[str], # every names of a student in order
 5    has_paid_fees: Callable[[Sequence[str]], bool],
 6    generate_unique_email: Callable[[Sequence[str]], str],
 7) -> str | None:
 8    if has_paid_fees(one_student_names):
 9        return generate_unique_email(one_student_names)
10    return None
11
12email: str|None = admit_student(
13    ["firstname", "lastname"],
14    lambda _: True,
15    lambda _: "firstname.lastname@domain.fr",
16)
17
18email_: str|None = admit_student(
19    ["firstname", "lastname"],
20    lambda _: "True",           # not ok
21    lambda _: "firstname.lastname@domain.fr",
22)