Python · Type Hints & Typing

Python Type Hints Explained — Annotations, Optional, and mypy

4 min read Updated 2026-06-19

Practice Type Hints & Annotations interview questions

Python type hints, explained

Type hints let you annotate what types your functions and variables expect — improving editor autocomplete, catching bugs before runtime, and documenting intent. The catch that interviewers love: Python does not enforce them at runtime. Understanding what hints do and don't do is the heart of this topic. This guide covers the syntax and the tooling.

Hints are not enforced at runtime

This is the single most important fact: type hints are annotations, not runtime checks. The interpreter records them but does not validate them — passing the "wrong" type runs fine unless your own code breaks.

def greet(name: str) -> str:
    return "hi " + name

greet(42)        # TypeError — but from the +, NOT from the hint
                 # Python never checked that 42 is a str

Hints exist for humans and tools: editors use them for autocomplete and warnings, and static checkers like mypy verify them before you run. Enforcement is opt-in via those external tools (or libraries like Pydantic), never automatic.

Basic annotations

Annotate parameters, return types, and variables with name: Type. The return type goes after ->.

def add(a: int, b: int) -> int:
    return a + b

count: int = 0
names: list[str] = []
ratio: float = 1.5

These are visible at runtime in __annotations__, which is how tools like dataclasses and Pydantic read them — but again, nothing is checked automatically.

Optional and Union (and the | syntax)

Optional[X] means "X or None" — common for default-None arguments. Union[X, Y] means "either type." Since Python 3.10 you can write X | None and X | Y directly.

from typing import Optional, Union

def find(id: int) -> Optional[str]:      # may return a str or None
    ...

def parse(x: Union[int, str]) -> int:    # accepts int or str
    ...

# 3.10+ shorthand:
def find(id: int) -> str | None: ...
def parse(x: int | str) -> int: ...

Optional[X] is exactly Union[X, None] — it does not mean "optional argument," just "could be None." A checker will then force you to handle the None case before using the value.

Built-in generics: listint, dictstr, int

Since Python 3.9 you annotate containers with the built-in types directly — list[int], dict[str, int], tuple[int, ...] — instead of importing List, Dict from typing (which are now deprecated aliases).

def totals(scores: dict[str, list[int]]) -> dict[str, int]:
    return {name: sum(vals) for name, vals in scores.items()}

The lowercase built-in forms are the modern style; the capitalised typing.List/Dict are only needed on Python 3.8 and earlier.

Any vs object

Any is an escape hatch: it's compatible with everything in both directions, so the checker stops checking — use it sparingly. object is the actual base of all types, so you can assign anything to it but can only do object-level operations without a cast.

from typing import Any

x: Any = "anything"
x.foo().bar()        # checker says nothing — Any disables checking

y: object = "hi"
y.upper()            # checker ERROR — object has no .upper(); narrow it first

Rule of thumb: Any means "trust me, stop checking"; object means "literally any object, but stay type-safe." Prefer object (or a precise type) and reserve Any for genuinely dynamic boundaries.

What mypy does

mypy (and similar checkers like Pyright) is a static analysis tool: it reads your hints and flags type mismatches without running the code. This is where hints turn into actual bug-catching.

def double(n: int) -> int:
    return n * 2

double("oops")       # mypy: error: Argument 1 to "double" has incompatible type "str"
mypy yourmodule.py   # run the checker; it never executes your program

Add it to CI and you catch a whole class of None-handling and wrong-type bugs before they reach production — the real payoff of annotating your code.

Recap

Type hints are annotations, not runtime checks — Python records but never enforces them; the value comes from editors and static checkers like mypy. Annotate with name: Type and -> ReturnType, use Optional[X]/X | None for "maybe None" and Union/X | Y for alternatives, and prefer built-in generics (list[int], dict[str, int]) on modern Python. Treat Any as an escape hatch that disables checking and object as the safe "anything" type. Hints don't change how your program runs — they make bugs visible before it runs.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.