You write a helper that returns the first item of a list. It should work
for any list, so you annotate it with object, the type every value has:
def first(items: list[object]) -> object:
return items[0]
name = first(["ada", "grace"])
print(name.upper())
The program runs and prints ADA. mypy still rejects it:
L6:7 [attr-defined]
"object" has no attribute "upper"
The function says it returns an object, so that is all mypy knows about
name. The caller passed in strings, but the return type does not say so.
Annotating with Any would remove the error, but then
mypy would stop checking name at all.
Let mypy fill in the type at each call
Give the function a type parameter, a placeholder type that mypy fills in at each call:
from typing import reveal_type
def first[T](items: list[T]) -> T:
return items[0]
name = first(["ada", "grace"])
reveal_type(name)
reveal_type(first([3, 1, 2]))
print(name.upper())
[T] after the function name declares the type parameter. list[T] says
the argument is a list of some type, and -> T says the result has that same
type. At each call, mypy looks at the argument and works out what T is. A
function or class with a type parameter is called generic.
reveal_type asks mypy to print the type it worked out. Here mypy reports:
Revealed type is "str"
Revealed type is "int"
Now name.upper() passes, because mypy knows that name is a str. When
the program runs, reveal_type prints the value's type and returns the value
unchanged. It is a tool for exploring, so delete it when you are done.
The name T belongs to one function, so two generic functions can both use
T without clashing. Within one call, every place that says T means the
same type:
def last_two[T](items: list[T]) -> list[T]:
return items[-2:]
last_two(["a", "b", "c"]) has the type list[str], and
last_two([1.5, 2.5]) has the type list[float].
Common mistakes
Calling a method that not every type has. Inside the function, T
could be any type, so mypy allows only what every object supports:
def shout[T](item: T) -> T:
return item.upper()
"T" has no attribute "upper"
If the function works only on strings, take a str. A later exercise in
this track shows how to allow a family of types with a bound, such as
[T: Hashable].
Returning something that is not a T. An empty list has no first item.
Returning None for it fails, because the return type says the result is a
T:
Incompatible return value type (got "None", expected "T")
Put None in the return type instead. Then each caller checks for it, as in
the first exercise of this track:
def first[T](items: list[T]) -> T | None:
if not items:
return None
return items[0]
The older spelling
The [T] syntax needs Python 3.12 or newer. Older code creates the type
parameter with TypeVar, at the top of the module, and a generic class
inherits from Generic:
from typing import Generic, TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
class Box(Generic[T]):
def __init__(self, item: T) -> None:
self.item = item
Both spellings mean the same thing to mypy. In the new syntax, the class is
class Box[T]:. You will meet both in existing code. The exercises in this
track ask for the new one, and two of their starters use the old one.
Check your understanding
first is declared as def first[T](items: list[T]) -> T. What type does mypy give x in x = first([2, 4, 6])?
Show answer
Hide answer
int. The argument is a list[int], so T is int for this call, and the
return type T becomes int. If first returned object, mypy would know
only that x is some object, and x + 1 would be an error.
Going further
- Using
Ttwice does not force two arguments to have the same type. Fordef pair[T](a: T, b: T) -> list[T], the callpair(1, "a")passes, and mypy picks a type that fits both, hereobject. - mypy's generics guide covers generic classes, bounds and variance in more depth.