Skip to content

Type Hints & Annotations Interview Questions & Answers

15 questions Updated 2026-06-18 Share:

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 mypy(opens in new tab)
15 of 15

No. 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.

Use Callable[[ArgTypes], ReturnType] from typing (or collections.abc). The first element is the list of argument types, the second the return type. Use ... for "any arguments."

from typing import Callable

def apply(fn: Callable[[int, int], int], a: int, b: int) -> int:
    return fn(a, b)

handler: Callable[[str], None]          # takes str, returns None
any_callable: Callable[..., int]        # any args, returns int

Rule of thumb: Callable[[args], ret] types higher-order functions/callbacks; use ... for the arg list when signatures vary or don't matter.

Assign a type to a name for reuse and readability; Python 3.12 adds the explicit type statement, and typing.TypeAlias annotates one pre-3.12. Aliases make complex types self-documenting.

from typing import TypeAlias

Vector: TypeAlias = list[float]          # pre-3.12 explicit alias
Matrix = list[list[float]]               # simple assignment also works

# Python 3.12+:
type UserId = int
type Json = dict[str, "Json"] | list["Json"] | str | int | bool | None

Rule of thumb: alias complex/repeated types (Vector, Json) for clarity; use the 3.12 type statement where available, else X: TypeAlias = ....

x: MyClass means an instance of the class; x: type[MyClass] means the class object itself (or a subclass) — used when you pass classes around, e.g. for factories.

class Animal: ...
class Dog(Animal): ...

def feed(a: Animal) -> None: ...        # takes an instance
def make(cls: type[Animal]) -> Animal:  # takes the class
    return cls()

make(Dog)        # OK — Dog is a type[Animal]
feed(Dog())      # OK — an instance

Rule of thumb: type[X] for "the class (or subclass) of X" (factories, registries); bare X for "an instance of X."

Literal["a", "b"] restricts a value to specific constants (great for modes/ flags). Final marks a name that must not be reassigned (a constant), enforced by the type checker.

from typing import Literal, Final

def open_mode(mode: Literal["r", "w", "a"]) -> None: ...
open_mode("r")          # OK
open_mode("x")          # type error — not an allowed literal

MAX_SIZE: Final = 100
MAX_SIZE = 200          # type error — can't reassign a Final

Rule of thumb: Literal to constrain to an exact set of values; Final to declare constants the checker will protect from reassignment.

TypedDict types a dict with a fixed set of string keys and per-key value types — ideal for JSON-like records where you want a dict (not a class) but still want type checking on keys/values.

from typing import TypedDict

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

u: User = {"name": "Ada", "age": 36}     # checked
u2: User = {"name": "Bob"}               # error — missing 'age'

class Partial(TypedDict, total=False):    # all keys optional
    nickname: str

Rule of thumb: use TypedDict to type structured dicts (API payloads, config) while keeping plain-dict ergonomics; use total=False for optional keys.

Use typing.Self (3.11+) for methods returning the instance's own type — cleaner than a string forward reference and correct under subclassing. Before 3.11, use a string literal ("MyClass") or a TypeVar bound to the class.

from typing import Self

class Builder:
    def add(self, x) -> Self:    # returns the same (sub)class
        ...
        return self

class SubBuilder(Builder): ...
SubBuilder().add(1)              # inferred as SubBuilder, not Builder

Rule of thumb: return Self for fluent/builder methods and alternative constructors so subclasses get the correct return type.

A forward reference annotates a type not yet defined (e.g. a class referring to itself) using a string. from __future__ import annotations makes all annotations strings (lazy), so you can drop the quotes and avoid definition-order issues.

class Node:
    def __init__(self, next: "Node | None" = None):  # quoted forward ref
        self.next = next

# or, at the top of the file:
from __future__ import annotations
class Node:
    def __init__(self, next: Node | None = None):    # no quotes needed
        self.next = next

Rule of thumb: quote self/forward references, or use from __future__ import annotations to make all annotations lazy strings (then read them via typing.get_type_hints).

Annotate the element type: *args: int means each positional arg is an int (args is a tuple[int, ...]), and **kwargs: str means each keyword value is a str (kwargs is a dict[str, str]).

def f(*args: int, **kwargs: str) -> None:
    # args: tuple[int, ...], kwargs: dict[str, str]
    ...

f(1, 2, 3, name="ada")        # ints positionally, str values by keyword
f(1, "x")                     # type error — "x" isn't int

Rule of thumb: annotate *args/**kwargs with the type of each item, not the tuple/dict — the checker infers the container.

Use -> None for functions that return nothing (procedures), and -> NoReturn (or Never in 3.11+) for functions that never return normally — they always raise or loop forever.

from typing import NoReturn

def log(msg: str) -> None:        # returns nothing useful
    print(msg)

def fail(msg: str) -> NoReturn:   # always raises
    raise RuntimeError(msg)

Rule of thumb: -> None for "no return value"; -> NoReturn/Never for functions that always raise or never terminate (helps the checker's flow analysis).

Gradual typing lets you add hints piecemeal — unannotated code is treated as Any and not checked, so you can type the most critical modules first. Tools like mypy support per-module strictness to ratchet up coverage.

# type the public API first, leave internals untyped for now
def public_api(user_id: int) -> str:
    return _helper(user_id)        # _helper untyped -> treated as Any

def _helper(x):                    # no hints yet — not checked
    return str(x)
# mypy.ini — enforce strictness only where ready
[mypy-myapp.api.*]
disallow_untyped_defs = True

Rule of thumb: adopt types gradually from the boundaries inward; use per-module mypy config to enforce strictness only where the code is fully annotated.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel