Advertisement
🚀 Advanced Python

Python Type Hinting – Type Annotations and mypy

Type hints are optional annotations that declare the expected types of function parameters, return values, and variables. They don't affect runtime behaviour — Python still runs untyped code. But they enable static analysis tools like mypy, better IDE autocomplete, and self-documenting code.

⏱️ 20 min read🎯 Advanced📅 Updated 2026

Basic Type Annotations

Annotate function parameters and return types with : type and -> type.

Python
# Before type hints (ambiguous)
def add(a, b):
    return a + b

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

def greet(name: str, times: int = 1) -> str:
    return (f"Hello, {name}! " * times).strip()

# Variable annotations
age: int = 25
name: str = "Alice"
prices: list[float] = [1.99, 2.49, 0.99]
💡
Tip

Type hints are optional and not enforced at runtime. Use them for documentation and tooling benefits.

The typing Module

For complex types use Optional, Union, Callable, Tuple.

Python
from typing import Optional, Union, Callable, Tuple

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

# Union: one of several types
def process(value: Union[int, float, str]) -> str:
    return str(value)

# Callable: function type
def apply(func: Callable[[int], int], n: int) -> int:
    return func(n)

# Tuple with specific types
Point = Tuple[float, float]
def distance(p: Point) -> float:
    return (p[0]**2 + p[1]**2) ** 0.5
▶ Output
None "42" 25.0

Modern Type Hints (Python 3.10+)

Python 3.10+ allows cleaner syntax for Union and Optional.

Python
# Python 3.10+ — use | instead of Union
def process(value: int | float | str) -> str:
    return str(value)

# Optional is just X | None
def find(id: int) -> str | None:
    return None

# Built-in generics (no need to import from typing)
def average(numbers: list[float]) -> float:
    return sum(numbers) / len(numbers)

def get_config() -> dict[str, str]:
    return {"host": "localhost"}
Advertisement

Type Hints with dataclasses

Type hints are required in dataclasses to define fields.

Python
from dataclasses import dataclass
from typing import ClassVar

@dataclass
class Product:
    name: str
    price: float
    quantity: int = 0
    tax_rate: ClassVar[float] = 0.08  # Class variable

    def total_price(self) -> float:
        return self.price * (1 + self.tax_rate)

p = Product("Coffee", 3.50, 10)
print(p.name, p.total_price())
▶ Output
Coffee 3.78

Type Hints: Documentation Python Doesn't Enforce

The biggest surprise about type hints: Python ignores them at runtime. They're annotations for humans and tools, not checks. Pass a string where you hinted int and it runs fine — nothing raises. The value comes from static checkers like mypy and editor autocomplete.

def greet(name: str, times: int = 1) -> str:
    return (name + " ") * times

greet(123, "oops")   # runs! hints are NOT validated at runtime
HintMeans
list[int]list of ints
dict[str, float]str keys → float values
Optional[str] / str | Nonea str or None
Callable[[int], str]function int → str

Why bother? On a big codebase, mypy catches "you passed the wrong type" before you ever run the program, and your editor can autocomplete and refactor safely. Start by hinting function signatures — that's where the payoff is highest. To actually validate at runtime you need a library like pydantic.

🏋️ Practical Exercise

Add type hints to your code:

  1. Annotate a function add(a: int, b: int) -> int.
  2. Hint a variable holding a list of strings using list[str].
  3. Use Optional (or | None) for a parameter that may be None.
  4. Run mypy on the file and fix any reported type errors.

🔥 Challenge Exercise

Take an untyped function that processes a list of user records (dicts) and fully annotate it: use list, dict, Optional, and a TypedDict or dataclass for the record shape, plus a return type. Then introduce a deliberate type mismatch and confirm mypy catches it at check time even though Python runs it fine. Bonus: add a Callable type hint for a function passed as an argument.

📋 Summary

  • Type hints annotate the expected types of variables, parameters, and return values.
  • They are not enforced at runtime — Python ignores them during execution.
  • Static checkers like mypy use them to catch type errors before running.
  • The typing module provides Optional, Union, Callable, and more (some now built-in).
  • Python 3.9+ allows built-in generics (list[int]); 3.10+ allows X | Y union syntax.
  • Hints improve readability, editor autocompletion, and refactoring safety.

Interview Questions on Type Hinting

  • What are type hints and are they enforced at runtime?
  • What is the benefit of type hints if Python ignores them?
  • What is the typing module used for?
  • What is the difference between Optional[X] and X | None?
  • What is a static type checker like mypy?
  • How do you annotate a function that takes another function?
  • What changed about type hints in Python 3.9 and 3.10?

FAQ

Do type hints affect how my program runs? +

No. Python treats annotations as metadata and does not enforce them at runtime — a function hinted to take an int will still accept a string. Their value comes from static checkers, editors, and documentation.

If they are not enforced, why bother? +

Hints catch bugs early via tools like mypy, power IDE autocompletion and refactoring, and serve as always-accurate documentation. On large codebases they greatly reduce type-related errors.

What is the difference between Optional[int] and int | None? +

They mean the same thing — a value that is an int or None. Optional comes from typing; the int | None syntax is the newer, cleaner form available in Python 3.10+.

What is mypy? +

mypy is a static type checker that reads your annotations and reports type inconsistencies without running the code. Adding it to CI catches a whole class of bugs before they reach production.