Type Hints

Intermediate · 18 min read · ▶ live playground · ✦ checkpoint

Python will let you pass "forty-two" to a parameter you meant to be a number. Type hints give you a way to write that expectation directly into the function, so humans and tools can spot the mismatch before it becomes a bug.

Annotate the function signature

A type hint is a label in your code that says what kind of value you expect. An annotation is the syntax Python uses for that label.

In a function, a parameter annotation goes after a parameter name with :, and a return annotation goes after -> before the final colon. The whole def line is the function signature, which means the part that shows the function name, parameters, and returned shape.

def score_label(player: str, score: int) -> str:
    return f"{player}: {score}"
 
print(score_label("Mina", 42))
print(score_label("Ravi", "thirty-five"))
print(score_label.__annotations__)

Mina: 42
Ravi: thirty-five
{'player': <class 'str'>, 'score': <class 'int'>, 'return': <class 'str'>}

Read the first line as: player should be a str, score should be an int, and the function should return a str.

The __annotations__ dictionary shows that Python stores the hints as metadata. Metadata is information about the code, not the main data your program is processing.

Hints do not enforce types at runtime

Runtime is when your program is actually running. Python does not enforce type hints at runtime. The second call above passed a string where the hint said int, and Python still ran the function because the function body could format that value into text.

That does not make hints fake. It means they have a different job.

def add_bonus(score: int, bonus: int = 10) -> int:
    return score + bonus
 
print(add_bonus(40))
print(add_bonus("40", " points"))

50
40 points

add_bonus("40", " points") works because + can join two strings. The hints did not convert "40" into 40, and they did not stop the call.

Type hints are labels on the recipe card from 7.1. They say "this blank expects an integer" or "this recipe returns text." They are not a locked door. Python still runs the instructions you wrote.

So why write them?

  • They help you read a function without scanning the whole body.
  • They help editors suggest methods and catch suspicious calls.
  • They make function expectations visible while you read.
  • They give future you a faster way back into the code.

Hint common value shapes

Use the same type names you already know: int for integers, str for strings, bool for booleans, and float for floating-point numbers. For a list, put the item type in square brackets: list[int] means "a list of integers."

def total_points(scores: list[int]) -> int:
    total = 0
    for score in scores:
        total = total + score
    return total
 
def shout_names(players: list[str]) -> list[str]:
    shouted = []
    for player in players:
        shouted.append(player.upper())
    return shouted
 
print(total_points([10, 20, 5]))
print(shout_names(["Mina", "Ravi"]))

35
['MINA', 'RAVI']

list[int] does not mean "any list." It means every item should be an integer. list[str] means every item should be a string.

You can add hints gradually. A small script might not need them everywhere. A function that other code calls often is a better place to start, because its signature is a promise many readers depend on.

Use Optional for None

Sometimes a function returns a real value when it finds one, and None when it does not. Optional[int] means "an int or None."

The from typing import Optional line imports a standard-library name. An import brings a name from another Python module, an importable bundle of reusable names and tools, into your file; modules get a full lesson in 8.1.

from typing import Optional
 
def first_passing_score(scores: list[int]) -> Optional[int]:
    for score in scores:
        if score >= 40:
            return score
    return None
 
print(first_passing_score([35, 42, 48]))
print(first_passing_score([12, 29]))

42
None

Optional[int] does not mean the argument is optional. It means the value might be missing, and Python represents that missing value with None, the reserved empty seat from lesson 2.2.

You may also see int | None in modern Python code. It means the same idea. This lesson uses Optional[int] because many docs and codebases still use it, and the name makes the beginner meaning visible.

Try it - make the contract visible

Run this roster formatter, then change the data and the fallback. The code works like ordinary Python, but the hints make each function's promise visible before you read the body.

python — playgroundlive
⌘/Ctrl + Enter to run

This example also shows dict[str, int]: a dictionary whose keys should be strings and whose values should be integers. You do not need to memorize every collection hint now. The pattern is enough: outer shape first, item shapes inside brackets.

Read hints as contracts

A contract is an expectation two pieces of code agree on. Type hints are contracts for readers and tools. They tell you what should go in and what should come out.

They do not replace tests. They do not replace clear names. They do not make Python a different language. They add one more layer of communication right where a function begins.

For now, aim for this habit:

  • Annotate parameters on functions that other functions call.
  • Annotate return values when the result is not obvious.
  • Use Optional[...] whenever None is a normal possible result.
  • Keep the body honest: if the hint says -> str, return text on every path.

Checkpoint

Answer all three to mark this lesson complete

You now have the whole beginner function toolkit: define, call, pass, return, scope, higher-order behavior, and readable type contracts. Next, the course opens the door to modules, where you split code across files and use Python's standard library on purpose.

+50 XP on completion