Type Hints & Annotations Interview Questions & Answers
5 questions Updated 2026-06-18
Python interview questions on type hints, Optional and Union, generics with list vs List, typing.Any vs object, mypy, and Protocol structural typing.
Read the in-depth guidePython Type Hints Explained — Annotations, Optional, and mypyNo. Type hints are annotations, not constraints — the interpreter
stores them (in __annotations__) but never checks them. You can pass
a str where an int is annotated and Python runs it happily; enforcement
is the job of an external static type checker like mypy or pyright.
def double(n: int) -> int:
return n * 2
double("ab") # runs fine -> "abab", no TypeError
double.__annotations__ # {'n': <class 'int'>, 'return': <class 'int'>}
If you want runtime validation you opt in explicitly — e.g. pydantic,
typing.get_type_hints, or manual isinstance checks. Rule of thumb: hints
document and enable tooling; they are not a runtime guard.
Union[A, B] means "A or B". Optional[X] is just shorthand for
Union[X, None] — a value that may be X or None. It does not
mean "optional argument"; it means "could be None". Since Python 3.10 you
can write unions with the | operator instead of importing from typing.
from typing import Optional, Union
def find(id: int) -> Optional[str]: ... # str or None
def parse(x: Union[int, str]) -> int: ... # int or str
# Python 3.10+ equivalent, no imports:
def find(id: int) -> str | None: ...
def parse(x: int | str) -> int: ...
Prefer the modern X | None syntax on 3.10+. Reach for Optional/Union
from typing only when supporting older versions. Rule of thumb: Optional
is about nullability, never about whether a parameter has a default.
Both annotate a list, but List comes from typing while list is
the built-in. Since Python 3.9 the built-in containers are themselves
subscriptable (list[int], dict[str, int]), so typing.List,
typing.Dict, etc. are deprecated — use the lowercase built-ins. A bare
list means "list of anything"; the generic form pins the element type.
from typing import List # legacy
names: List[str] = []
names: list[str] = [] # modern (3.9+), preferred
scores: dict[str, int] = {}
pair: tuple[int, str] = (1, "a")
Generics let a checker verify element access and method calls. Rule of thumb:
on 3.9+ always parameterize the built-in (list[str]), and only import
from typing for things with no built-in equivalent (e.g. Callable).
Both accept any value, but they are opposites to a type checker. object
is the real base of every class — you can assign anything to it, but you
can only do object-level operations on it. Any is an escape hatch: it
is compatible with everything in both directions, so the checker stops
checking — any attribute or call is allowed.
def f(x: object) -> None:
x.upper() # type error: object has no 'upper'
def g(x: Any) -> None:
x.upper() # OK — Any disables checking
x + 1 # also OK, no complaints
Use object when you genuinely accept anything but want to keep type
safety (forcing you to narrow with isinstance first). Use Any only to
deliberately opt out of checking. Rule of thumb: Any is contagious and
hides bugs — prefer object or a precise type.
mypy is a static type checker: it reads your annotations and flags
type mismatches before you run the code — no execution, no runtime cost.
By default it checks types nominally (by inheritance). typing.Protocol
adds structural typing (a.k.a. duck typing): a class matches a Protocol
if it has the right methods/attributes, even without inheriting from it.
from typing import Protocol
class Closable(Protocol):
def close(self) -> None: ...
def shutdown(r: Closable) -> None:
r.close()
class File: # never imports/inherits Closable
def close(self) -> None: ...
shutdown(File()) # OK — File structurally matches
So mypy verifies correctness, and Protocol lets it accept anything with
the right shape rather than a specific base class. Rule of thumb: use
Protocols to type "anything that behaves like X" without forcing a
common base class.
Practice tests are coming soon
Get notified when interactive mock interviews and quizzes launch.