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.
The Any Type
Use Any when a value can be anything (avoid overusing it):
primitives.py
# Primitive type hints
name: str =
age: int =
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 =
age: int =
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 =
age: int =
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 =
age: int =
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 =
age: int =
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 =
age: int =
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 =
age: int =
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 =
age: int =
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 =
age: int =
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}")
function_signatures.py
# 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))
return_types.py
# 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))
none_return.py
# 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"))
any.py
# 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"))
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.
type annotation - syntax `variable: Type = value` that declares the expected type of a variable or parameter
function signature - the combination of parameter types and return type that defines a function's contract
None return - functions that perform actions but don't return meaningful values use `-> None`
Any - a type that accepts any value, used when the type is truly dynamic or unknown
Exercise: practical.py
Add type hints to a user registration function