Type Hints

Type Hints

1. What are Type Hints?

Type hints are a feature introduced in Python 3.5+ that allow you to explicitly specify the types of variables and functions. While Python remains a dynamically typed language, type hints can improve code readability and stability.

# Without type hints
def greet(name):
    return f"Hello, {name}"

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

Advantages of Type Hints

Advantage Description
Readability Clearly shows function input/output types
IDE Support Enables autocomplete and type error warnings
Documentation Code serves as its own documentation
Bug Prevention Detect errors before runtime through static analysis

2. Basic Type Hints

Primitive Types

# Variable type hints
name: str = "Python"
age: int = 30
price: float = 19.99
is_active: bool = True
data: bytes = b"hello"

# Function type hints
def add(a: int, b: int) -> int:
    return a + b

def say_hello(name: str) -> None:
    print(f"Hello, {name}")

Collection Types

# Python 3.9+ (direct use of built-in types)
numbers: list[int] = [1, 2, 3]
names: set[str] = {"Alice", "Bob"}
scores: dict[str, int] = {"math": 95, "english": 88}
point: tuple[int, int] = (10, 20)
coordinates: tuple[float, ...] = (1.0, 2.0, 3.0)  # Variable length

# Python 3.8 and below (using typing module)
from typing import List, Set, Dict, Tuple

numbers: List[int] = [1, 2, 3]
names: Set[str] = {"Alice", "Bob"}
scores: Dict[str, int] = {"math": 95}
point: Tuple[int, int] = (10, 20)

3. typing Module

Optional

Indicates that a value can be None.

from typing import Optional

def find_user(user_id: int) -> Optional[str]:
    """Find user or return None"""
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)

# Python 3.10+ alternative
def find_user(user_id: int) -> str | None:
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)

Union

Allows one of multiple types.

from typing import Union

def process(value: Union[int, str]) -> str:
    """Accept int or str and convert to string"""
    return str(value)

# Python 3.10+ alternative
def process(value: int | str) -> str:
    return str(value)

Any

Allows any type (disables type checking).

from typing import Any

def log(message: Any) -> None:
    print(message)

# Any value is allowed
log("hello")
log(123)
log([1, 2, 3])

Callable

Specifies function types.

from typing import Callable

# Callable[[argument_types], return_type]
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
    return func(a, b)

def add(x: int, y: int) -> int:
    return x + y

result = apply(add, 3, 4)  # 7

TypeVar

Defines generic type variables.

from typing import TypeVar, List

T = TypeVar('T')

def first(items: List[T]) -> T:
    """Return first element of list"""
    return items[0]

# Type is automatically inferred
num = first([1, 2, 3])      # int
name = first(["a", "b"])    # str

4. Advanced Type Hints

Generic Classes

from typing import Generic, TypeVar

T = TypeVar('T')

class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []

    def push(self, item: T) -> None:
        self._items.append(item)

    def pop(self) -> T:
        return self._items.pop()

    def is_empty(self) -> bool:
        return len(self._items) == 0

# Usage
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)

str_stack: Stack[str] = Stack()
str_stack.push("hello")

TypedDict

Defines dictionary key and value types.

from typing import TypedDict

class User(TypedDict):
    name: str
    age: int
    email: str

# Requires exact keys and types
user: User = {
    "name": "Alice",
    "age": 30,
    "email": "alice@example.com"
}

# Optional keys
class UserOptional(TypedDict, total=False):
    name: str
    age: int
    nickname: str  # Optional

Protocol (Structural Subtyping)

Express duck typing with type hints.

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None:
        ...

class Circle:
    def draw(self) -> None:
        print("Drawing circle")

class Square:
    def draw(self) -> None:
        print("Drawing square")

def render(shape: Drawable) -> None:
    shape.draw()

# Circle and Square don't inherit from Drawable
# but they can be used because they have draw() method
render(Circle())  # OK
render(Square())  # OK

Literal

Allows only specific literal values.

from typing import Literal

def set_mode(mode: Literal["read", "write", "append"]) -> None:
    print(f"Mode: {mode}")

set_mode("read")    # OK
set_mode("write")   # OK
# set_mode("delete")  # Type error!

Final

Indicates constants (no reassignment).

from typing import Final

MAX_SIZE: Final = 100
PI: Final[float] = 3.14159

# MAX_SIZE = 200  # Type checker warns

5. Type Aliases

Give names to complex types.

from typing import Dict, List, Tuple

# Type aliases
UserId = int
UserData = Dict[str, str]
UserList = List[Tuple[UserId, UserData]]

def get_users() -> UserList:
    return [
        (1, {"name": "Alice", "email": "alice@example.com"}),
        (2, {"name": "Bob", "email": "bob@example.com"}),
    ]

# Python 3.10+ TypeAlias
from typing import TypeAlias

Vector: TypeAlias = list[float]
Matrix: TypeAlias = list[Vector]

6. Runtime vs Static Analysis

Type Hints Are Not Enforced at Runtime

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

# Runs without issues at runtime!
result = greet(123)  # "Hello, 123"

Static Type Checker (mypy)

# Install
pip install mypy

# Run type check
mypy your_script.py
# example.py
def add(a: int, b: int) -> int:
    return a + b

result = add("hello", "world")  # mypy detects error
$ mypy example.py
example.py:4: error: Argument 1 to "add" has incompatible type "str"; expected "int"

Runtime Type Checking (Optional)

from typing import get_type_hints

def validate_types(func):
    """Runtime type validation decorator"""
    hints = get_type_hints(func)

    def wrapper(*args, **kwargs):
        # Validate argument types
        for (name, value), expected_type in zip(
            kwargs.items(), hints.values()
        ):
            if not isinstance(value, expected_type):
                raise TypeError(f"{name} must be {expected_type}")
        return func(*args, **kwargs)
    return wrapper

7. Practical Patterns

API Response Type Definition

from typing import TypedDict, List, Optional

class APIResponse(TypedDict):
    success: bool
    data: Optional[dict]
    error: Optional[str]

class User(TypedDict):
    id: int
    name: str
    email: str

class UsersResponse(TypedDict):
    users: List[User]
    total: int
    page: int

def fetch_users(page: int = 1) -> UsersResponse:
    # API call logic
    return {
        "users": [{"id": 1, "name": "Alice", "email": "alice@example.com"}],
        "total": 100,
        "page": page
    }

Configuration Class

from typing import Final, ClassVar

class Config:
    # Class variable (shared across instances)
    DEBUG: ClassVar[bool] = False
    VERSION: ClassVar[str] = "1.0.0"

    # Constants
    MAX_CONNECTIONS: Final = 100
    TIMEOUT: Final[int] = 30

Overload

Define multiple signatures for the same function.

from typing import overload, Union

@overload
def process(value: int) -> int: ...

@overload
def process(value: str) -> str: ...

def process(value: Union[int, str]) -> Union[int, str]:
    if isinstance(value, int):
        return value * 2
    return value.upper()

# Type checker infers correct return type
num: int = process(5)       # int
text: str = process("hi")   # str

8. Commonly Used Types Summary

Type Description Example
int, str, float, bool Basic types x: int = 1
list[T] List nums: list[int]
dict[K, V] Dictionary data: dict[str, int]
set[T] Set ids: set[int]
tuple[T, ...] Tuple point: tuple[int, int]
Optional[T] T or None name: Optional[str]
Union[T1, T2] T1 or T2 value: Union[int, str]
Callable[[Args], R] Function type fn: Callable[[int], str]
Any Any type data: Any
TypeVar Generic variable T = TypeVar('T')
Protocol Structural typing class Sized(Protocol)

9. Practice Problems

Exercise 1: Function Type Hints

Add appropriate type hints to the following function.

def calculate_average(numbers):
    if not numbers:
        return None
    return sum(numbers) / len(numbers)

Exercise 2: Generic Function

Write a generic function that finds the first element satisfying a condition in a list.

# find_first([1, 2, 3, 4], lambda x: x > 2) -> 3
# find_first(["a", "bb", "ccc"], lambda s: len(s) > 1) -> "bb"

Exercise 3: TypedDict

Define a TypedDict representing a user profile. - Required: id (int), username (str), email (str) - Optional: bio (str), avatar_url (str)


Next Steps

Check out 02_Decorators.md to learn about decorators that extend functions and classes!

to navigate between lessons