Skip to content
← Back to explainers

Python explainer

Generic Functions from Zero

About 4 min to read. Allow extra time to work through examples.

Write a helper that works on a list of any type and still tells mypy what it returns. Learn type parameters, reveal_type, and the older TypeVar spelling you will see in existing code.

This article uses Python. You can make it your practice mode to browse related content across the site.

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

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 T twice does not force two arguments to have the same type. For def pair[T](a: T, b: T) -> list[T], the call pair(1, "a") passes, and mypy picks a type that fits both, here object.
  • mypy's generics guide covers generic classes, bounds and variance in more depth.

step 10 of 26

Precise Type Hints with Literals and Generics

Up next Partition and Chunk a List with Generic Helpers