Type Hints

Python · Lesson 44 of 56 7 min read Updated

Python does not make you declare types. A variable can hold a number now and a string later. That freedom is great for quick scripts, but in bigger programs it becomes hard to remember what a function expects. Does make_bill() want the price as a number or as text? Does it return a string or a float?

Type hints answer those questions inside the code. They are notes like price: float that tell readers, editors and checking tools what type a value should have. Modern Python code uses them everywhere, and so will the rest of this course.

Annotating variables and functions

Write a colon and a type after a variable or parameter name. Write -> and a type before the colon of a def line to describe the return value.

PythonRun it yourself
city: str = "Kochi"
pin_code: int = 682001
rating: float = 4.6
is_open: bool = True

def add_gst(price: float, rate: int = 18) -> float:
    return round(price * (100 + rate) / 100, 2)

def greet(name: str) -> None:
    print(f"Namaste, {name}")

print(add_gst(1299, 5))
greet("Meera")
print(add_gst.__annotations__)
Output
1363.95
Namaste, Meera
{'price': <class 'float'>, 'rate': <class 'int'>, 'return': <class 'float'>}

-> None means the function returns nothing useful. Python stores the hints in a special attribute called __annotations__, which tools can read. An int is accepted wherever a float is expected, so add_gst(1299, 5) is fine.

Inside a function you rarely need to annotate local variables: checkers work out that total = 0 is an int on their own. Annotate function parameters and return values first; they give the most benefit.

Hints are not enforced at runtime

This surprises many beginners: Python itself ignores type hints when running your code. A wrong type does not raise an error at the moment of the call.

PythonRun it yourself
def total_marks(marks: list[int]) -> int:
    return sum(marks)

print(total_marks([78, 85, 91]))
print(total_marks((1.5, 2.5)))      # wrong type, but Python runs it anyway
Output
254
4.0

To catch such mistakes you use a type checker, a separate program that reads your code without running it. The most popular are mypy and pyright (pyright also powers the Python support in VS Code). After installing one with pip (see pip & Virtual Environments), you run, for example, mypy marks.py, and it reports that the second call passes a tuple of floats where a list of integers is expected. Your editor shows the same warnings as red underlines while you type.

Lists, dictionaries and other collections

For collections, put the type of the items in square brackets after the built-in name. This works directly on list, dict, set and tuple; you do not need to import anything.

HintMeaning
list[str]A list of strings
dict[str, int]A dictionary with string keys and integer values
set[int]A set of integers
tuple[str, int]A tuple of exactly two items: a string, then an integer
tuple[float, ...]A tuple of any length, all floats
PythonRun it yourself
def class_topper(results: dict[str, int]) -> tuple[str, int]:
    name = max(results, key=results.get)
    return name, results[name]

def roll_numbers(names: list[str], start: int = 1) -> dict[int, str]:
    return {number: name for number, name in enumerate(names, start=start)}

print(class_topper({"Aarav": 82, "Priya": 94, "Kabir": 77}))
print(roll_numbers(["Isha", "Rohan", "Ananya"], start=101))
Output
('Priya', 94)
{101: 'Isha', 102: 'Rohan', 103: 'Ananya'}

You may see List[str] and Dict[str, int] imported from the typing module in older code. Since Python 3.9 the lowercase built-in names are preferred.

Optional values with X | None

Many functions return a value or None when nothing is found. Write that with the | operator, which means "or": str | None. The same operator combines any types, as in int | float.

PythonRun it yourself
def find_phone(contacts: dict[str, str], name: str) -> str | None:
    return contacts.get(name)

contacts = {"Farhan": "98100 12345", "Isha": "99990 54321"}

for person in ["Isha", "Kabir"]:
    phone = find_phone(contacts, person)
    if phone is None:
        print(f"No number saved for {person}")
    else:
        print(f"{person}: {phone}")
Output
Isha: 99990 54321
No number saved for Kabir

A type checker will now warn you if you use the result as a string without first checking for None, which prevents a very common crash. Older code writes Optional[str] from the typing module, which means the same as str | None.

Two more useful names: Any from typing turns checking off for a value, and Callable from collections.abc describes a function, for example Callable[[float], float] for a function that takes a float and returns a float.

Type aliases and generics

When a hint gets long or carries meaning, give it a name with the type statement (Python 3.12 and later). This creates a type alias:

PythonRun it yourself
type Rupees = int
type Menu = dict[str, Rupees]

def bill(menu: Menu, order: list[str]) -> Rupees:
    return sum(menu[item] for item in order)

canteen: Menu = {"samosa": 15, "chai": 12, "poha": 30}
print(bill(canteen, ["chai", "samosa", "chai"]))
print(Rupees.__value__)
Output
39
<class 'int'>

A generic function works with many types but keeps them connected. In first_item below, [T] after the name introduces a type variable T. The hint says "you give me a list of some type T, and I return a T". If you pass a list of strings, the checker knows the result is a string.

PythonRun it yourself
def first_item[T](items: list[T]) -> T:
    return items[0]

class Shelf[T]:
    def __init__(self) -> None:
        self.items: list[T] = []

    def put(self, item: T) -> None:
        self.items.append(item)

print(first_item(["Pune", "Jaipur"]).upper())
print(first_item([4, 9, 2]) + 1)

books: Shelf[str] = Shelf()
books.put("Godan")
print(books.items)
Output
PUNE
5
['Godan']

class Shelf[T]: makes a generic class in the same way. This square-bracket syntax arrived in Python 3.12; older code creates type variables with T = TypeVar("T").

Common mistakes

  • Expecting Python to reject wrong types. Hints are not checked at runtime. Run mypy or pyright, or validate values yourself.
  • Writing list(int) or list<int>. The syntax is square brackets: list[int].
  • Using None as a default without saying so. def f(limit: int = None) is wrong; write limit: int | None = None.
  • Annotating everything. Focus on function signatures and class attributes; let the checker infer simple local variables.

Key points

  • Hints describe types: name: str, def f(x: int) -> str:, -> None for no result.
  • Python ignores hints at runtime; checkers such as mypy and pyright and your editor use them to find bugs.
  • Use built-in generics like list[int], dict[str, float] and tuple[str, int].
  • X | None marks values that may be missing; | also combines types such as int | float.
  • The type statement creates aliases, and def f[T](...) or class C[T]: write generic code (Python 3.12+).
  • Python 3.14 evaluates annotations lazily, so classes can refer to themselves in hints without quotes.

Exercise

Add complete type hints to this function and test it: def average_by_city(students) receives a list of (name, city, marks) tuples, and returns a dictionary from city name to average marks rounded to 1 place. Create a type alias Student for the tuple type. Then call it with four students from Pune and Lucknow and print the result.

Show solution
Python
type Student = tuple[str, str, int]

def average_by_city(students: list[Student]) -> dict[str, float]:
    totals: dict[str, list[int]] = {}
    for _name, city, marks in students:
        totals.setdefault(city, []).append(marks)
    return {city: round(sum(m) / len(m), 1) for city, m in totals.items()}

data: list[Student] = [
    ("Aarav", "Pune", 81),
    ("Isha", "Lucknow", 92),
    ("Rohan", "Pune", 74),
    ("Farhan", "Lucknow", 69),
]
print(average_by_city(data))
Output
{'Pune': 77.5, 'Lucknow': 80.5}

The alias Student names the tuple shape once and makes the signature easy to read. The empty dictionary totals is annotated because a checker cannot guess its types from {} alone.

Quick Quiz

1. What happens when you call def f(x: int) with a string, without a type checker?

2. Which hint means "a list of integers"?

3. How do you write "a string, or None"?

4. What does def first[T](items: list[T]) -> T: describe?

Learn Python with a mentor

Free lessons get you started. Our Python Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call