Type Hints
Type Hints Basics
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
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}")
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
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
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
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
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))
print("add(2, 3) =", add(2, 3))
9print("add(2, 3) =", add(2, 3))def add(a: int, b: int) -> int:
5# Basic function6def add(a2: int, b3: int) -> int:7 return a2 + b3print("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) = 5def 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))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)def format_user(*, name: str, age: int) -> str:
28def format_user(*, name: str, age: int) -> str:29 return f"{nameAlice} ({age30})"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))
print(greet("World"))
10print(greet("World"))def greet(name: str) -> str:
7def greet(nameWorld: str) -> str:8 return f"Hello, {nameWorld}"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, Worlddef 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)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.0users ← {1: 'Alice', 2: 'Bob'}
pass 1 of 223def 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)print("find_user(1) =", find_user(1))
28print("find_user(1) =", find_user(1))29print("find_user(99) =", find_user(99))outputfind_user(1) = Aliceusers ← {1: 'Alice', 2: 'Bob'}
pass 2 of 223def 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)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"))
log("starting")
10log("starting")def log(message: str) -> None:
7def log(messagestarting: str) -> None:8 print(f"[LOG] {messagestarting}")output[LOG] startingitems ← []
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")tags ← ['python']
pass 1 of 214def add_tag(tags[]: list[str], tagpython: str) -> None:15 tags→ ['python'].append(tagpython)items ← ['python']
17items: list[str] = []18add_tag(items→ ['python'], "python")19add_tag(items['python'], "typing")20print("tags:", items)tags ← ['python', 'typing']
pass 2 of 214def add_tag(tags['python']: list[str], tagtyping: str) -> None:15 tags→ ['python', 'typing'].append(tagtyping)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']def try_parse_int(text: str) -> Optional[int]:
pass 1 of 224def try_parse_int(text123: str) -> Optional[int]:25 try:26 return int(text)try:
pass 1 of 224def try_parse_int(text: str) -> Optional[int]:25 try:26 return int(text123)27 except ValueError: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') = 123def try_parse_int(text: str) -> Optional[int]:
pass 2 of 224def try_parse_int(textabc: str) -> Optional[int]:25 try:26 return int(text)try:
pass 2 of 224def try_parse_int(text: str) -> Optional[int]:25 try:26 return int(textabc)27 except ValueError: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"))
pretty_print(123)
10pretty_print(123)11pretty_print("hello")def pretty_print(value: Any) -> None:
pass 1 of 57def 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 pass value1 123 2 hello 3 {'a': 1} 4 [1, 2, 3] 5 {'name': 'Alice', 'age': 30, 'tags': ['python', 'typing'], 'meta': {'active': True}} pretty_print(123)
10pretty_print(123)11pretty_print("hello")12pretty_print({"a": 1})pretty_print("hello")
10pretty_print(123)11pretty_print("hello")12pretty_print({"a": 1})13pretty_print([1, 2, 3])pretty_print({"a": 1})
11pretty_print("hello")12pretty_print({"a": 1})13pretty_print([1, 2, 3])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}})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"))v ← Alice
pass 1 of 233def 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 ""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 = Alicev ← None
pass 2 of 233def 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 ""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