Why it matters
Suppose you need a function that prints a value twice. You could write one version for text and another for numbers. Both copies have the same two lines, and when you find a bug, you must fix it in both.
Python does not need copies. A function works on any object that supports the operations it uses, so one def already handles text, numbers and lists. What Python does not do is check the types of a call before the code runs. A function's author states the expected types in type hints, which are notes written after parameter names and after ->. The hints do not change what the code does. A separate tool, a type checker such as mypy, reads the hints and reports mismatches before the program runs.
A generic function is a function whose hints say that one type placeholder is used in several places. This lesson shows how to write one, how a bound limits which types a placeholder may stand for, and which mistakes Python reports at run time.
The output in this lesson was produced with Python 3.14. The [T] syntax needs Python 3.12 or newer.
The idea: a placeholder for a type
A type parameter is a name that stands for a type you do not know yet. The usual name is one capital letter, T (for "type"). You declare it in square brackets after the function name:
def print_twice[T](item: T) -> None:
print(item)
print(item)Read it in parts. [T] says "this function has a type called T". item: T says the parameter item has type T. -> None says the function gives back nothing. When you call the function, a type checker works out what T is from the arguments you pass.
Here is a complete program:
Output:
hi
hi
42
42In the first call, T is str, because the argument is text. In the second call, T is int. The body is written once, and the caller's argument decides the type. Python runs the same code in both calls. The hints are not used at run time.
Code written before Python 3.12 creates the same kind of placeholder with TypeVar from the typing module. You will see it in older projects.
Using the type in the return value
A generic function can give back a value of type T. The caller then gets a value of the same type it passed in.
Think of a shelf of books. You ask for the first book on the shelf, and if the shelf is empty you get a default. The kind of thing on the shelf does not change the rule.
Output:
nobody
AshaThe parameter items has the hint list[T], which means a list of whatever T is. The fallback has the hint T. A type checker requires both to match, so a list of strings with a number as the fallback is reported before the program runs. Python itself would run that call without complaint, as the mistakes section shows.
The first call uses an empty list, so the function gives back "nobody". The second call adds "Asha" first, so the function gives back the first item.
Type bounds: saying what the type must support
A plain T can stand for any type. That is a problem when the body uses an operation that only some types have. Here is a function that gives back the larger of two values:
def largest[T](a: T, b: T) -> T:
if a < b:
return b
return aThe < operator works on numbers and text, but a plain T says nothing about <. A type checker reports the line. Python runs the function, and a call with two plain objects fails when < runs. The mistakes section shows that error.
A bound says which methods the placeholder must have. You write it after a colon inside the brackets, as in [T: Bound]. Python has no built-in class that means "can be compared with <", so you write a Protocol. A Protocol is a class that lists the methods a type must have. A type matches the Protocol when it has those methods. It does not have to inherit from the Protocol.
Output:
12
banana__lt__ is the method that Python calls for <. A name that starts and ends with two underscores is a special method, which Python calls for an operator. int and str both have it, so both match Orderable. Any means the other value may have any type. The ... on the method line means that no body is written there.
Java programmers will recognize the idea of a bound. Where Java writes Comparable, Python uses a Protocol that lists the method the operator calls.
Bounds on numbers
For numbers, a union bound is often enough. The hint int | float means "either an int or a float". The built-in sum adds the numbers in a list:
Output:
245
3.75Python keeps the result as an int when every number is an int, so the first result prints as 245, not 245.0.
A second example uses two type parameters. A dictionary maps keys to values. The function finds the key with the largest value. Keys can be any type, because the function never compares them. Values must support <:
Output:
Ravimax looks at the keys, and key=mapping.get gives it each key's value to compare. The bound V: Orderable applies to V only. K has no bound because the keys are never compared with <.
Several rules on one type
Some functions need a type that is both a number and comparable. In Python, a union bound already limits the type to two concrete types, and both support <. This function gives back a value no larger than a maximum:
Output:
100
2.5If a bound must require several methods, write one Protocol that lists all of them.
Calling a generic function
In a normal call you never write the types. The type checker works them out from the arguments, and Python runs the function without them. Python does not accept explicit types in a call to a function. Writing first_or_default[str](["x"], "y") raises TypeError: 'function' object is not subscriptable at run time.
If you need to make a type clear to a reader, write it on the variable that holds the value, as the names: list[str] line did in the first_or_default example.
Common mistakes
Expecting the hints to stop a wrong argument
def add_one(x: int) -> int:
return x + 1
print(add_one("a"))Python reports:
Traceback (most recent call last):
File "/path/to/main.py", line 5, in <module>
print(add_one("a"))
~~~~~~~^^^^^
File "/path/to/main.py", line 2, in add_one
return x + 1
~~^~~
TypeError: can only concatenate str (not "int") to strThe hint x: int does nothing at run time. The error comes from +, when the code runs. A type checker would report the call before the program runs.
Using a placeholder that was never declared
Output:
1No error appears, because T is never declared, but Python 3.14 does not evaluate the hints when a function is defined. The error appears only when a program reads the hints:
import typing
def f(x: T) -> T:
return x
print(typing.get_type_hints(f))Python reports, at the end of its traceback:
NameError: name 'T' is not definedA type checker reports the undefined name before the program runs. Declare the placeholder in brackets after the function name: def f[T](x: T) -> T:.
Using an operation that the function does not allow
def pick_larger[T](a: T, b: T) -> T:
return a if a > b else b
print(pick_larger(object(), object()))Python reports:
Traceback (most recent call last):
File "/path/to/main.py", line 5, in <module>
print(pick_larger(object(), object()))
~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
File "/path/to/main.py", line 2, in pick_larger
return a if a > b else b
^^^^^
TypeError: '>' not supported between instances of 'object' and 'object'The bound is missing, so the hints allow any type. Add a bound that lists the operation, such as [T: Orderable], and a type checker will reject the call before the program runs. The run-time error is still raised by Python if the call gets past the checker.
Passing a type that does not meet the bound
A bound is a rule for the caller as well as the function. If largest is declared with [T: Orderable], every argument must have <. A type checker reports a call with arguments that do not match. At run time, Python raises a TypeError from the < inside the function, and the last line of the traceback names the operation that failed.
Practice
Each exercise has its answer below it. Try to write your answer before you read it.
Exercise 1
Write a generic function swap_first_two[T](items: list[T]) -> None that swaps the first two items of a list in place. Call it once with ["red", "blue", "green"] and once with [1, 2, 3]. Print the list before and after each call.
Show the answer
Output:
['red', 'blue', 'green']
['blue', 'red', 'green']
[1, 2, 3]
[2, 1, 3]The line items[0], items[1] = items[1], items[0] swaps the two items without a temporary variable. The function needs no bound, because it never compares items.
Exercise 2
Write smallest_of[T: Orderable](items: list[T]) -> T that gives back the smallest item. Use the Orderable Protocol from the bounds section. Test it with [8, 3, 5] and with ["kiwi", "apple", "mango"].
Show the answer
Output:
3
appleThe function starts with the first item as the smallest, then compares each other item with <. items[1:] is a slice: all items from position 1 to the end.
Exercise 3
Write count_above[N: int | float](numbers: list[N], limit: float) -> int that counts how many numbers are greater than limit. Call it with [4, 9, 12] and a limit of 8.5.
Show the answer
Output:
2The numbers 9 and 12 are greater than 8.5.
Exercise 4
The function below has no bound, so it works only for types that support >. Call it with two object() values to see the error. Then add a bound that makes the function's requirement visible in the hints. Use a Protocol that lists the method behind <, and change the body to use <.
def pick_larger[T](a: T, b: T) -> T:
return a if a > b else bShow the answer
Output:
8
pearThe bound T: Orderable lists the method that < needs, so a type checker will refuse arguments that lack it. At run time, pick_larger(object(), object()) still fails with a TypeError, because the object does not support <.
Exercise 5
Python lists are not typed when the code runs, so a list of T needs no special rule. Write make_list[T](n: int) -> list[T | None] that gives back a list of n values, all None. Call it with 3.
Show the answer
Output:
[None, None, None]T | None in the hint means "a value of type T, or None". The list is filled with None because the function has no value of type T to put in it.
Summary
- A type parameter such as
[T]after the function name stands for a type that the caller decides at each call. - Type hints such as
item: Tand-> Tare read by a type checker. Python does not check them when the code runs. - A bound such as
[T: Orderable]says which methods the type must have. Write a Protocol when no built-in class fits. - A union bound such as
[T: int | float]limits a type to a fixed set of types. - A missing bound or an undeclared placeholder can run without error until the code reaches the operation or the hint is read.
- You do not pass types in a call. The checker infers them from the arguments.