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