When you write a function that processes user data, how do you know what types of values to pass? Type hints solve this by documenting expected types directly in your code. They catch bugs before runtime, improve IDE autocompletion, and make your code self-documenting for teammates.

Python type hints (introduced in PEP 484 and expanded since) let you annotate variables, parameters, and return values. Type hints are optional at runtime, but they improve readability, documentation, and tooling (IDE autocompletion, static analysis with tools like mypy).

Primitive Type Annotations

example
primitives.py
Replay: real traced execution (multi-file project)
# Primitive type hints

name: str = "Alice"
age: int = 30
height_m: float = 1.68
is_student: bool = False

print("Primitives:")
print(f"  name={name} ({type(name).__name__})")
print(f"  age={age} ({type(age).__name__})")
print(f"  height_m={height_m} ({type(height_m).__name__})")
print(f"  is_student={is_student} ({type(is_student).__name__})")


# Reassignment (still allowed at runtime)
age = age + 1
print("\nAfter birthday:")
print(f"  age={age}")

# Mixed operations
bmi: float = 70.0 / (height_m ** 2)
print("\nBMI:")
print(f"  bmi={bmi:.2f}")
# Primitive type hints

name: str = "Maya"
age: int = 30
height_m: float = 1.68
is_student: bool = False

print("Primitives:")
print(f"  name={name} ({type(name).__name__})")
print(f"  age={age} ({type(age).__name__})")
print(f"  height_m={height_m} ({type(height_m).__name__})")
print(f"  is_student={is_student} ({type(is_student).__name__})")


# Reassignment (still allowed at runtime)
age = age + 1
print("\nAfter birthday:")
print(f"  age={age}")

# Mixed operations
bmi: float = 70.0 / (height_m ** 2)
print("\nBMI:")
print(f"  bmi={bmi:.2f}")
# Primitive type hints

name: str = "Jordan"
age: int = 30
height_m: float = 1.68
is_student: bool = False

print("Primitives:")
print(f"  name={name} ({type(name).__name__})")
print(f"  age={age} ({type(age).__name__})")
print(f"  height_m={height_m} ({type(height_m).__name__})")
print(f"  is_student={is_student} ({type(is_student).__name__})")


# Reassignment (still allowed at runtime)
age = age + 1
print("\nAfter birthday:")
print(f"  age={age}")

# Mixed operations
bmi: float = 70.0 / (height_m ** 2)
print("\nBMI:")
print(f"  bmi={bmi:.2f}")
# Primitive type hints

name: str = "Alice"
age: int = 17
height_m: float = 1.68
is_student: bool = False

print("Primitives:")
print(f"  name={name} ({type(name).__name__})")
print(f"  age={age} ({type(age).__name__})")
print(f"  height_m={height_m} ({type(height_m).__name__})")
print(f"  is_student={is_student} ({type(is_student).__name__})")


# Reassignment (still allowed at runtime)
age = age + 1
print("\nAfter birthday:")
print(f"  age={age}")

# Mixed operations
bmi: float = 70.0 / (height_m ** 2)
print("\nBMI:")
print(f"  bmi={bmi:.2f}")
# Primitive type hints

name: str = "Alice"
age: int = 45
height_m: float = 1.68
is_student: bool = False

print("Primitives:")
print(f"  name={name} ({type(name).__name__})")
print(f"  age={age} ({type(age).__name__})")
print(f"  height_m={height_m} ({type(height_m).__name__})")
print(f"  is_student={is_student} ({type(is_student).__name__})")


# Reassignment (still allowed at runtime)
age = age + 1
print("\nAfter birthday:")
print(f"  age={age}")

# Mixed operations
bmi: float = 70.0 / (height_m ** 2)
print("\nBMI:")
print(f"  bmi={bmi:.2f}")
  1. name ← Alice, age ← 30, height_m ← 1.68, is_student ← False, bmi ← 24.801587301587304

    3name→ Alice: str = "Alice"4#@name="Maya", "Jordan"5age→ 30: int = 306#@age=17, 457height_m→ 1.68: float = 1.688is_student→ False: bool = FalseFalse910print("Primitives:")11print(f"  name={nameAlice} ({type(name).__name__})")12print(f"  age={age30} ({type(age).__name__})")13print(f"  height_m={height_m1.68} ({type(height_m).__name__})")14print(f"  is_student={is_studentFalse} ({type(is_student).__name__})")1516#@help h117# Type hints annotate intent; they don't enforce runtime types.18# IDEs and type checkers can validate these annotations.19# Use simple types for readability.20#@end2122# Reassignment (still allowed at runtime)23age→ 31 = age + 124print("\nAfter birthday:")25print(f"  age={age31}")2627# Mixed operations28bmi→ 24.801587301587304: float = 70.0 / (height_m1.68 ** 2)29print("\nBMI:")30print(f"  bmi={bmi24.801587301587304:.2f}")
    outputPrimitives:
      name=Alice (str)
      age=30 (int)
      height_m=1.68 (float)
      is_student=False (bool)
    
    After birthday:
      age=31
    
    BMI:
      bmi=24.80
  1. name ← Maya, age ← 30, height_m ← 1.68, is_student ← False, bmi ← 24.801587301587304

    3name→ Maya: str = "Maya"4age→ 30: int = 305height_m→ 1.68: float = 1.686is_student→ False: bool = FalseFalse78print("Primitives:")9print(f"  name={nameMaya} ({type(name).__name__})")10print(f"  age={age30} ({type(age).__name__})")11print(f"  height_m={height_m1.68} ({type(height_m).__name__})")12print(f"  is_student={is_studentFalse} ({type(is_student).__name__})")131415# Reassignment (still allowed at runtime)16age→ 31 = age + 117print("\nAfter birthday:")18print(f"  age={age31}")1920# Mixed operations21bmi→ 24.801587301587304: float = 70.0 / (height_m1.68 ** 2)22print("\nBMI:")23print(f"  bmi={bmi24.801587301587304:.2f}")
    outputPrimitives:
      name=Maya (str)
      age=30 (int)
      height_m=1.68 (float)
      is_student=False (bool)
    
    After birthday:
      age=31
    
    BMI:
      bmi=24.80
  1. name ← Jordan, age ← 30, height_m ← 1.68, is_student ← False, bmi ← 24.801587301587304

    3name→ Jordan: str = "Jordan"4age→ 30: int = 305height_m→ 1.68: float = 1.686is_student→ False: bool = FalseFalse78print("Primitives:")9print(f"  name={nameJordan} ({type(name).__name__})")10print(f"  age={age30} ({type(age).__name__})")11print(f"  height_m={height_m1.68} ({type(height_m).__name__})")12print(f"  is_student={is_studentFalse} ({type(is_student).__name__})")131415# Reassignment (still allowed at runtime)16age→ 31 = age + 117print("\nAfter birthday:")18print(f"  age={age31}")1920# Mixed operations21bmi→ 24.801587301587304: float = 70.0 / (height_m1.68 ** 2)22print("\nBMI:")23print(f"  bmi={bmi24.801587301587304:.2f}")
    outputPrimitives:
      name=Jordan (str)
      age=30 (int)
      height_m=1.68 (float)
      is_student=False (bool)
    
    After birthday:
      age=31
    
    BMI:
      bmi=24.80
  1. name ← Alice, age ← 17, height_m ← 1.68, is_student ← False, bmi ← 24.801587301587304

    3name→ Alice: str = "Alice"4age→ 17: int = 175height_m→ 1.68: float = 1.686is_student→ False: bool = FalseFalse78print("Primitives:")9print(f"  name={nameAlice} ({type(name).__name__})")10print(f"  age={age17} ({type(age).__name__})")11print(f"  height_m={height_m1.68} ({type(height_m).__name__})")12print(f"  is_student={is_studentFalse} ({type(is_student).__name__})")131415# Reassignment (still allowed at runtime)16age→ 18 = age + 117print("\nAfter birthday:")18print(f"  age={age18}")1920# Mixed operations21bmi→ 24.801587301587304: float = 70.0 / (height_m1.68 ** 2)22print("\nBMI:")23print(f"  bmi={bmi24.801587301587304:.2f}")
    outputPrimitives:
      name=Alice (str)
      age=17 (int)
      height_m=1.68 (float)
      is_student=False (bool)
    
    After birthday:
      age=18
    
    BMI:
      bmi=24.80
  1. name ← Alice, age ← 45, height_m ← 1.68, is_student ← False, bmi ← 24.801587301587304

    3name→ Alice: str = "Alice"4age→ 45: int = 455height_m→ 1.68: float = 1.686is_student→ False: bool = FalseFalse78print("Primitives:")9print(f"  name={nameAlice} ({type(name).__name__})")10print(f"  age={age45} ({type(age).__name__})")11print(f"  height_m={height_m1.68} ({type(height_m).__name__})")12print(f"  is_student={is_studentFalse} ({type(is_student).__name__})")131415# Reassignment (still allowed at runtime)16age→ 46 = age + 117print("\nAfter birthday:")18print(f"  age={age46}")1920# Mixed operations21bmi→ 24.801587301587304: float = 70.0 / (height_m1.68 ** 2)22print("\nBMI:")23print(f"  bmi={bmi24.801587301587304:.2f}")
    outputPrimitives:
      name=Alice (str)
      age=45 (int)
      height_m=1.68 (float)
      is_student=False (bool)
    
    After birthday:
      age=46
    
    BMI:
      bmi=24.80
type annotation - syntax `variable: Type = value` that declares the expected type of a variable or parameter

Function Signatures

function_signatures.py
Replay: real traced execution (multi-file project)
# Function signatures with type hints

from typing import Tuple

# Basic function
def add(a: int, b: int) -> int:
    return a + b

print("add(2, 3) =", add(2, 3))

# Multiple return values

def min_max(values: list[int]) -> Tuple[int, int]:
    if not values:
        raise ValueError("values must not be empty")
    return (min(values), max(values))

print("min_max([3, 1, 9]) =", min_max([3, 1, 9]))


# Keyword-only example

def format_user(*, name: str, age: int) -> str:
    return f"{name} ({age})"

print("format_user(name='Alice', age=30) =", format_user(name="Alice", age=30))
  1. print("add(2, 3) =", add(2, 3))

    9print("add(2, 3) =", add(2, 3))
  2. def add(a: int, b: int) -> int:

    5# Basic function6def add(a2: int, b3: int) -> int:7    return a2 + b3
  3. print("add(2, 3) =", add(2, 3))

    9print("add(2, 3) =", add(2, 3))1011# Multiple return values1213def min_max(values: list[int]) -> Tuple[int, int]:14    if not values:15        raise ValueError("values must not be empty")16    return (min(values), max(values))1718print("min_max([3, 1, 9]) =", min_max([3, 1, 9]))
    outputadd(2, 3) = 5
  4. def min_max(values: list[int]) -> Tuple[int, int]:

    13def min_max(values[3, 1, 9]: list[int]) -> Tuple[int, int]:14    if not values:15        raise ValueError("values must not be empty")16    return (min(values[3, 1, 9]), max(values))
  5. print("min_max([3, 1, 9]) =", min_max([3, 1, 9]))

    18print("min_max([3, 1, 9]) =", min_max([3, 1, 9]))1920#@help h121# Annotate function boundaries first: inputs and outputs.22# Tools can catch mismatched calls early.23# Use list[int] (Python 3.9+) and Tuple from typing if needed.24#@end2526# Keyword-only example2728def format_user(*, name: str, age: int) -> str:29    return f"{name} ({age})"3031print("format_user(name='Alice', age=30) =", format_user(name="Alice", age=30))
    outputmin_max([3, 1, 9]) = (1, 9)
  6. def format_user(*, name: str, age: int) -> str:

    28def format_user(*, name: str, age: int) -> str:29    return f"{nameAlice} ({age30})"
  7. print("format_user(name='Alice', age=30) =", format_user(name="Alice",…

    31print("format_user(name='Alice', age=30) =", format_user(name="Alice", age=30))
    outputformat_user(name='Alice', age=30) = Alice (30)
function signature - the combination of parameter types and return type that defines a function's contract

Return Types

return_types.py
Replay: real traced execution (multi-file project)
# Return types

from typing import Optional

# String return

def greet(name: str) -> str:
    return f"Hello, {name}"

print(greet("World"))

# Numeric return

def average(values: list[float]) -> float:
    if not values:
        return 0.0
    return sum(values) / len(values)

print("average([1.0, 2.0, 3.0]) =", average([1.0, 2.0, 3.0]))

# Optional return

def find_user(user_id: int) -> Optional[str]:
    # pretend DB
    users: dict[int, str] = {1: "Alice", 2: "Bob"}
    return users.get(user_id)

print("find_user(1) =", find_user(1))
print("find_user(99) =", find_user(99))

  1. print(greet("World"))

    10print(greet("World"))
  2. def greet(name: str) -> str:

    7def greet(nameWorld: str) -> str:8    return f"Hello, {nameWorld}"
  3. print(greet("World"))

    10print(greet("World"))1112# Numeric return1314def average(values: list[float]) -> float:15    if not values:16        return 0.017    return sum(values) / len(values)1819print("average([1.0, 2.0, 3.0]) =", average([1.0, 2.0, 3.0]))
    outputHello, World
  4. def average(values: list[float]) -> float:

    14def average(values[1.0, 2.0, 3.0]: list[float]) -> float:15    if not values:16        return 0.017    return sum(values[1.0, 2.0, 3.0]) / len(values)
  5. print("average([1.0, 2.0, 3.0]) =", average([1.0, 2.0, 3.0]))

    19print("average([1.0, 2.0, 3.0]) =", average([1.0, 2.0, 3.0]))2021# Optional return2223def find_user(user_id: int) -> Optional[str]:24    # pretend DB25    users: dict[int, str] = {1: "Alice", 2: "Bob"}26    return users.get(user_id)2728print("find_user(1) =", find_user(1))29print("find_user(99) =", find_user(99))
    outputaverage([1.0, 2.0, 3.0]) = 2.0
  6. users ← {1: 'Alice', 2: 'Bob'}

    pass 1 of 2
    23def find_user(user_id1: int) -> Optional[str]:24    # pretend DB25    users→ {1: 'Alice', 2: 'Bob'}: dict[int, str] = {1: "Alice", 2: "Bob"}26    return users{1: 'Alice', 2: 'Bob'}.get(user_id1)
  7. print("find_user(1) =", find_user(1))

    28print("find_user(1) =", find_user(1))29print("find_user(99) =", find_user(99))
    outputfind_user(1) = Alice
  8. users ← {1: 'Alice', 2: 'Bob'}

    pass 2 of 2
    23def find_user(user_id99: int) -> Optional[str]:24    # pretend DB25    users→ {1: 'Alice', 2: 'Bob'}: dict[int, str] = {1: "Alice", 2: "Bob"}26    return users{1: 'Alice', 2: 'Bob'}.get(user_id99)
  9. print("find_user(99) =", find_user(99))

    28print("find_user(1) =", find_user(1))29print("find_user(99) =", find_user(99))
    outputfind_user(99) = None

None Return Type

none_return.py
Replay: real traced execution (multi-file project)
# None return

from typing import Optional

# Logger

def log(message: str) -> None:
    print(f"[LOG] {message}")

log("starting")

# Mutating function

def add_tag(tags: list[str], tag: str) -> None:
    tags.append(tag)

items: list[str] = []
add_tag(items, "python")
add_tag(items, "typing")
print("tags:", items)

# Returning Optional

def try_parse_int(text: str) -> Optional[int]:
    try:
        return int(text)
    except ValueError:
        return None

print("try_parse_int('123') =", try_parse_int("123"))
print("try_parse_int('abc') =", try_parse_int("abc"))

  1. log("starting")

    10log("starting")
  2. def log(message: str) -> None:

    7def log(messagestarting: str) -> None:8    print(f"[LOG] {messagestarting}")
    output[LOG] starting
  3. items ← []

    10log("starting")1112# Mutating function1314def add_tag(tags: list[str], tag: str) -> None:15    tags.append(tag)1617items→ []: list[str] = []18add_tag(items[], "python")19add_tag(items, "typing")
  4. tags ← ['python']

    pass 1 of 2
    14def add_tag(tags[]: list[str], tagpython: str) -> None:15    tags→ ['python'].append(tagpython)
  5. items ← ['python']

    17items: list[str] = []18add_tag(items→ ['python'], "python")19add_tag(items['python'], "typing")20print("tags:", items)
  6. tags ← ['python', 'typing']

    pass 2 of 2
    14def add_tag(tags['python']: list[str], tagtyping: str) -> None:15    tags→ ['python', 'typing'].append(tagtyping)
  7. items ← ['python', 'typing']

    18add_tag(items, "python")19add_tag(items→ ['python', 'typing'], "typing")20print("tags:", items['python', 'typing'])2122# Returning Optional2324def try_parse_int(text: str) -> Optional[int]:25    try:26        return int(text)27    except ValueError:28        return None2930print("try_parse_int('123') =", try_parse_int("123"))31print("try_parse_int('abc') =", try_parse_int("abc"))
    outputtags: ['python', 'typing']
  8. def try_parse_int(text: str) -> Optional[int]:

    pass 1 of 2
    24def try_parse_int(text123: str) -> Optional[int]:25    try:26        return int(text)
  9. try:

    pass 1 of 2
    24def try_parse_int(text: str) -> Optional[int]:25    try:26        return int(text123)27    except ValueError:
  10. print("try_parse_int('123') =", try_parse_int("123"))

    30print("try_parse_int('123') =", try_parse_int("123"))31print("try_parse_int('abc') =", try_parse_int("abc"))
    outputtry_parse_int('123') = 123
  11. def try_parse_int(text: str) -> Optional[int]:

    pass 2 of 2
    24def try_parse_int(textabc: str) -> Optional[int]:25    try:26        return int(text)
  12. try:

    pass 2 of 2
    24def try_parse_int(text: str) -> Optional[int]:25    try:26        return int(textabc)27    except ValueError:
  13. print("try_parse_int('abc') =", try_parse_int("abc"))

    30print("try_parse_int('123') =", try_parse_int("123"))31print("try_parse_int('abc') =", try_parse_int("abc"))
    outputtry_parse_int('abc') = None
None return - functions that perform actions but don't return meaningful values use `-> None`

The Any Type

Use Any when a value can be anything (avoid overusing it):

any.py
Replay: real traced execution (multi-file project)
# Any type

from typing import Any

# Any values

def pretty_print(value: Any) -> None:
    print(f"value={value} (runtime type={type(value).__name__})")

pretty_print(123)
pretty_print("hello")
pretty_print({"a": 1})
pretty_print([1, 2, 3])


# JSON-like data
json_data: dict[str, Any] = {
    "name": "Alice",
    "age": 30,
    "tags": ["python", "typing"],
    "meta": {"active": True}
}

pretty_print(json_data)

# Safe extraction

def get_str(d: dict[str, Any], key: str) -> str:
    v = d.get(key)
    return v if isinstance(v, str) else ""

print("name =", get_str(json_data, "name"))
print("missing =", get_str(json_data, "missing"))
  1. pretty_print(123)

    10pretty_print(123)11pretty_print("hello")
  2. def pretty_print(value: Any) -> None:

    pass 1 of 5
    7def pretty_print(value123: Any) -> None:8    print(f"value={value123} (runtime type={type(value).__name__})")
    outputvalue=123 (runtime type=int)
    All 5 passes — pass 1 is the card above
    passvalue
    1123
    2hello
    3{'a': 1}
    4[1, 2, 3]
    5{'name': 'Alice', 'age': 30, 'tags': ['python', 'typing'], 'meta': {'active': True}}
  3. pretty_print(123)

    10pretty_print(123)11pretty_print("hello")12pretty_print({"a": 1})
  4. pretty_print("hello")

    10pretty_print(123)11pretty_print("hello")12pretty_print({"a": 1})13pretty_print([1, 2, 3])
  5. pretty_print({"a": 1})

    11pretty_print("hello")12pretty_print({"a": 1})13pretty_print([1, 2, 3])
  6. json_data ← {'name': 'Alice', 'age': 30, 'tags': ['python', 'typing'], 'meta': {'active': True}}

    12pretty_print({"a": 1})13pretty_print([1, 2, 3])1415#@help h116# Any disables type checking for that value.17# Use it when interfacing with dynamic data (e.g., JSON).18# Prefer specific types when possible.19#@end2021# JSON-like data22json_data→ {'name': 'Alice', 'age': 30, 'tags': ['python', 'typing'], 'meta': {'active': True}}: dict[str, Any] = {23    "name": "Alice",24    "age": 30,25    "tags": ["python", "typing"],26    "meta": {"active": True}27}2829pretty_print(json_data{'name': 'Alice', 'age': 30, 'tags': ['python', 'typing'], 'meta': {'active': True}})
  7. pretty_print(json_data)

    29pretty_print(json_data{'name': 'Alice', 'age': 30, 'tags': ['python', 'typing'], 'meta': {'active': True}})3031# Safe extraction3233def get_str(d: dict[str, Any], key: str) -> str:34    v = d.get(key)35    return v if isinstance(v, str) else ""3637print("name =", get_str(json_data{'name': 'Alice', 'age': 30, 'tags': ['python', 'typing'], 'meta': {'active': True}}, "name"))38print("missing =", get_str(json_data, "missing"))
  8. v ← Alice

    pass 1 of 2
    33def get_str(d{'name': 'Alice', 'age': 30, 'tags': ['python', 'typing'], 'meta': {'active': True}}: dict[str, Any], keyname: str) -> str:34    v→ Alice = d{'name': 'Alice', 'age': 30, 'tags': ['python', 'typing'], 'meta': {'active': True}}.get(keyname)35    return vAlice if isinstance(v, str) else ""
  9. print("name =", get_str(json_data, "name"))

    37print("name =", get_str(json_data{'name': 'Alice', 'age': 30, 'tags': ['python', 'typing'], 'meta': {'active': True}}, "name"))38print("missing =", get_str(json_data{'name': 'Alice', 'age': 30, 'tags': ['python', 'typing'], 'meta': {'active': True}}, "missing"))
    outputname = Alice
  10. v ← None

    pass 2 of 2
    33def get_str(d{'name': 'Alice', 'age': 30, 'tags': ['python', 'typing'], 'meta': {'active': True}}: dict[str, Any], keymissing: str) -> str:34    v→ None = d{'name': 'Alice', 'age': 30, 'tags': ['python', 'typing'], 'meta': {'active': True}}.get(keymissing)35    return vNone if isinstance(v, str) else ""
  11. print("missing =", get_str(json_data, "missing"))

    37print("name =", get_str(json_data, "name"))38print("missing =", get_str(json_data{'name': 'Alice', 'age': 30, 'tags': ['python', 'typing'], 'meta': {'active': True}}, "missing"))
    outputmissing = 
Any - a type that accepts any value, used when the type is truly dynamic or unknown

Notes

  • Type hints do not enforce types at runtime by default.
  • Use type hints as documentation and for static checking.
  • Start small: annotate function boundaries first.

Exercise: practical.py

Add type hints to a user registration function