Skip to main content

Python Runtime Type Checking Practice Problems & Exercises

Practice: Runtime Type Checking

11 problems3 Easy4 Medium4 Hard65–85 min
← Back to lesson

Easy

#1Safe Cast with isinstance GuardEasy
isinstanceruntime-checksafe-castUnion

Write a safe_describe(value) function that checks the runtime type with isinstance and returns a formatted description, with a fallback for unknown types.

Python
from typing import Any


def safe_describe(value: Any) -> str:
    if isinstance(value, bool):
        return f"Unknown type for {value}"
    elif isinstance(value, int):
        return f"{value} -> int"
    elif isinstance(value, float):
        return f"{value} -> float"
    elif isinstance(value, str):
        return f"{value} -> str"
    elif isinstance(value, list):
        return f"{value} -> list"
    else:
        return f"Unknown type for {value}"


values = [42, 3.14, "hello", [1, 2], True]
for v in values:
    print(safe_describe(v))
Expected Output
42 -> int
3.14 -> float
hello -> str
[1, 2] -> list
Unknown type for True
Hints

Hint 1: isinstance(value, (int, float, str, list)) checks against multiple types at once.

Hint 2: Check bool before int since bool is a subclass of int.


#2Read __annotations__ at RuntimeEasy
__annotations__runtimetype-hintsinspect

Inspect a class's type annotations at runtime using __annotations__ and typing.get_type_hints(). Check which fields are typed as str.

Python
from typing import get_type_hints


class User:
    name: str
    age: int
    email: str

    def __init__(self, name: str, age: int, email: str) -> None:
        self.name = name
        self.age = age
        self.email = email


raw = User.__annotations__
resolved = get_type_hints(User)

# Print a simplified version without module path
simple = {k: v.__name__ for k, v in resolved.items()}
print(f"User annotations: {simple}")
print(f"Fields: {list(resolved.keys())}")
str_fields = [k for k, v in resolved.items() if v is str]
print(f"All strings? {len(str_fields) == len(resolved)}")
Expected Output
User annotations: {'name': str, 'age': int, 'email': str}
Fields: ['name', 'age', 'email']
All strings? False
Hints

Hint 1: Class.__annotations__ returns the raw annotations dict. typing.get_type_hints() resolves forward references and includes inherited annotations.

Hint 2: The two may differ when forward references (strings) are used as annotation values.


#3Type Check Function Arguments with get_type_hintsEasy
get_type_hintsruntime-validationfunction-annotations

Write a decorator type_checked that reads a function's annotations at runtime and raises TypeError if any argument has the wrong type.

Python
import functools
import inspect
from typing import get_type_hints


def type_checked(func):
    hints = get_type_hints(func)
    sig = inspect.signature(func)

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        bound = sig.bind(*args, **kwargs)
        bound.apply_defaults()
        for name, value in bound.arguments.items():
            if name in hints and name != "return":
                expected = hints[name]
                if hasattr(expected, "__origin__"):
                    continue  # skip complex generics
                if not isinstance(value, expected):
                    raise TypeError(
                        f"{name} expected {expected.__name__}, "
                        f"got {type(value).__name__}"
                    )
        return func(*args, **kwargs)

    return wrapper


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


try:
    result = greet("Alice", 30)
    print(f"greet('Alice', 30) OK")
except TypeError as e:
    print(f"TypeError: {e}")

try:
    greet("Alice", "thirty")
except TypeError as e:
    print(f"TypeError: {e}")
Expected Output
greet('Alice', 30) OK
TypeError: age expected int, got str
Hints

Hint 1: Use get_type_hints(func) to get the parameter annotations. Pair them with the actual arguments via inspect.signature.

Hint 2: Compare isinstance(value, expected) for each annotated argument.


Medium

#4Runtime Dataclass ValidatorMedium
dataclassruntime-validation__post_init__get_type_hints

Build a ValidatedDataclass mixin that validates all field types in __post_init__ using get_type_hints. Apply it to a User dataclass.

Python
from dataclasses import dataclass
from typing import get_type_hints


class ValidatedDataclass:
    def __post_init__(self) -> None:
        hints = get_type_hints(type(self))
        for field_name, expected_type in hints.items():
            value = getattr(self, field_name)
            # Skip complex generics like Optional, Union, List, etc.
            if hasattr(expected_type, "__origin__"):
                continue
            if not isinstance(value, expected_type):
                raise TypeError(
                    f"{field_name}: expected {expected_type.__name__}, "
                    f"got {type(value).__name__}"
                )


@dataclass
class User(ValidatedDataclass):
    name: str
    age: int
    email: str


u = User(name="Alice", age=30, email="[email protected]")
print(u)

try:
    User(name="Bob", age="thirty", email="[email protected]")
except TypeError as e:
    print(f"TypeError: {e}")
Expected Output
User(name='Alice', age=30, email='[email protected]')
TypeError: age: expected int, got str
Hints

Hint 1: In __post_init__, call get_type_hints(type(self)) and for each field, check isinstance(getattr(self, field), expected_type).

Hint 2: Skip Optional fields or Union types that are harder to check — or handle them with get_args.


#5TypedDict Runtime ValidatorMedium
TypedDictruntime-validationget_type_hintsschema

Write a validate_typed_dict(data, schema) function that returns a list of all type and key errors found in a dict against a TypedDict schema.

Python
from typing import Dict, Any, List, get_type_hints
try:
    from typing import TypedDict
except ImportError:
    from typing_extensions import TypedDict


class PersonRecord(TypedDict):
    name: str
    age: int
    active: bool


def validate_typed_dict(data: Dict[str, Any], schema) -> List[str]:
    errors: List[str] = []
    hints = get_type_hints(schema)

    for key, expected_type in hints.items():
        if key not in data:
            errors.append(f"Missing key: {key}")
            continue
        value = data[key]
        # Skip complex generics
        if hasattr(expected_type, "__origin__"):
            continue
        if not isinstance(value, expected_type):
            errors.append(
                f"Wrong type for {key}: expected {expected_type.__name__}, "
                f"got {type(value).__name__}"
            )

    return errors


valid_data: Dict[str, Any] = {"name": "Alice", "age": 30, "active": True}
missing_age: Dict[str, Any] = {"name": "Alice", "active": True}
bad_name: Dict[str, Any] = {"name": 42, "age": 30, "active": True}

print(f"Valid: {validate_typed_dict(valid_data, PersonRecord) == []}")
print(f"Errors for missing age: {validate_typed_dict(missing_age, PersonRecord)}")
print(f"Errors for bad name type: {validate_typed_dict(bad_name, PersonRecord)}")
Expected Output
Valid: True
Errors for missing age: ['Missing key: age']
Errors for bad name type: ["Wrong type for name: expected str, got int"]
Hints

Hint 1: Use get_type_hints(TypedDictClass) to get the expected field types. Iterate over keys, check presence and isinstance.

Hint 2: Collect all errors in a list and return them all at once rather than raising on the first error.


#6Runtime Generic Type InspectionMedium
get_argsget_originruntimeGenericinspect

Use typing.get_origin and typing.get_args to inspect generic type aliases at runtime and print their structure.

Python
from typing import List, Dict, Optional, Union, get_origin, get_args
import typing


def describe_type(hint) -> str:
    origin = get_origin(hint)
    args = get_args(hint)
    if origin is None:
        name = getattr(hint, "__name__", str(hint))
        return f"{name}: not generic"
    origin_name = getattr(origin, "__name__", str(origin))
    if origin is Union:
        origin_name = "Union"
    arg_names = tuple(getattr(a, "__name__", str(a)) for a in args)
    return f"{hint}: origin={origin_name}, args={arg_names}"


hints = [
    List[int],
    Dict[str, float],
    Optional[str],
    int,
]

for h in hints:
    print(describe_type(h))
Expected Output
List[int]: origin=list, args=(int,)
Dict[str, float]: origin=dict, args=(str, float)
Optional[str]: origin=Union, args=(str, NoneType)
int: not generic
Hints

Hint 1: typing.get_origin(hint) returns the base generic (list, dict, Union, etc.) or None for non-generic types.

Hint 2: typing.get_args(hint) returns the type arguments as a tuple.


#7Enforce Annotations on Public MethodsMedium
get_type_hintsinspectenforcementruntime

Write check_annotations(cls) that verifies all public methods have return type annotations and all parameters (except self) are annotated.

Python
import inspect
from typing import get_type_hints, List


def check_annotations(cls) -> List[str]:
    errors: List[str] = []
    for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
        if name.startswith("_"):
            continue
        try:
            hints = get_type_hints(method)
        except Exception:
            hints = {}

        sig = inspect.signature(method)
        params = [
            p for p in sig.parameters
            if p != "self" and p != "cls"
        ]

        if "return" not in hints:
            errors.append(f"{name}: missing return annotation")

        for param in params:
            if param not in hints:
                errors.append(f"{name}: missing param annotation for {param}")

    return errors


class FullyAnnotated:
    def process(self, data: str) -> str:
        return data.upper()

    def transform(self, value: int) -> int:
        return value * 2


class StrictService:
    def process(self, data: str):   # missing return
        return data.upper()

    def transform(self, data) -> int:   # missing param annotation
        return len(data)


print(f"FullyAnnotated: {'OK' if not check_annotations(FullyAnnotated) else 'FAIL'}")
errors = check_annotations(StrictService)
print(f"Missing annotations on StrictService: {errors}")
Expected Output
FullyAnnotated: OK
Missing annotations on StrictService: ['process: missing return annotation', 'transform: missing param annotation for data']
Hints

Hint 1: Use inspect.getmembers(cls, predicate=inspect.isfunction) to enumerate methods.

Hint 2: get_type_hints(method) returns the annotation dict. Check for "return" key and each parameter name.


Hard

#8Deep Nested Type ValidatorHard
runtime-validationget_originget_argsrecursivenested

Write a recursive type_check(value, hint) function that validates complex nested types like List[int], Dict[str, float], and Optional[List[str]] at runtime.

Python
from typing import Any, List, Dict, Optional, Union, get_origin, get_args


def type_check(value: Any, hint: Any) -> tuple:
    """Returns (is_valid: bool, error_msg: str)"""
    origin = get_origin(hint)
    args = get_args(hint)

    if origin is None:
        if hint is type(None):
            ok = value is None
            return ok, ("" if ok else f"{value!r} is not None")
        if not isinstance(value, hint):
            name = getattr(hint, "__name__", str(hint))
            return False, f"{value!r} is not {name}"
        return True, ""

    if origin is Union:
        for arg in args:
            ok, _ = type_check(value, arg)
            if ok:
                return True, ""
        return False, f"{value!r} does not match {hint}"

    if origin is list:
        if not isinstance(value, list):
            return False, f"{value!r} is not a list"
        item_type = args[0] if args else Any
        if item_type is not Any:
            for item in value:
                ok, msg = type_check(item, item_type)
                if not ok:
                    name = getattr(item_type, "__name__", str(item_type))
                    return False, f"element {item!r} is not {name}"
        return True, ""

    if origin is dict:
        if not isinstance(value, dict):
            return False, f"{value!r} is not a dict"
        k_type, v_type = (args[0], args[1]) if len(args) == 2 else (Any, Any)
        for k, v in value.items():
            if k_type is not Any:
                ok, _ = type_check(k, k_type)
                if not ok:
                    kname = getattr(k_type, "__name__", str(k_type))
                    return False, f"key {k!r} is not {kname}"
            if v_type is not Any:
                ok, _ = type_check(v, v_type)
                if not ok:
                    vname = getattr(v_type, "__name__", str(v_type))
                    return False, f"value {v!r} is not {vname}"
        return True, ""

    return True, ""


cases = [
    ([1, 2, 3],     List[int],       "[1,2,3] valid as List[int]"),
    ([1, "a", 3],   List[int],       "[1,'a',3] valid as List[int]"),
    ({"a": 1},      Dict[str, int],  "{'a':1} valid as Dict[str,int]"),
    ({"a": "x"},    Dict[str, int],  "{'a':'x'} valid as Dict[str,int]"),
]

for value, hint, label in cases:
    ok, err = type_check(value, hint)
    if ok:
        print(f"{label}: True")
    else:
        print(f"{label}: False — {err}")
Expected Output
[1,2,3] valid as List[int]: True
[1,'a',3] valid as List[int]: False — element 'a' is not int
{'a':1} valid as Dict[str,int]: True
{'a':'x'} valid as Dict[str,int]: False — value 'x' is not int
Hints

Hint 1: Use get_origin to detect list, dict, Union, Optional and recurse into their args.

Hint 2: For Union/Optional, check if the value matches any of the args types.


#9Auto-Validating Pydantic-Like ModelHard
runtime-validationget_type_hintsmetaclasspydantic-style

Build a BaseModel with a metaclass that auto-generates a validated __init__ from annotations. It should raise ValidationError with field name, expected type, and actual value.

Python
from typing import get_type_hints


class ValidationError(Exception):
    pass


class ModelMeta(type):
    def __new__(mcs, name, bases, namespace):
        annotations = namespace.get("__annotations__", {})
        field_names = list(annotations.keys())

        def generated_init(self, **kwargs):
            hints = get_type_hints(type(self))
            for fname in field_names:
                if fname not in kwargs:
                    raise ValidationError(f"{fname}: missing required field")
                value = kwargs[fname]
                expected = hints.get(fname)
                if expected and not hasattr(expected, "__origin__"):
                    if not isinstance(value, expected):
                        raise ValidationError(
                            f"{fname}: expected {expected.__name__}, "
                            f"got {type(value).__name__} (value: {value!r})"
                        )
                setattr(self, fname, value)

        def generated_repr(self):
            parts = [f"{f}={getattr(self, f)!r}" for f in field_names]
            return f"{name}(" + ", ".join(parts) + ")"

        namespace["__init__"] = generated_init
        namespace["__repr__"] = generated_repr
        return super().__new__(mcs, name, bases, namespace)


class BaseModel(metaclass=ModelMeta):
    pass


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


u = User(name="Alice", age=30)
print(u)

try:
    User(name="Bob", age="thirty")
except ValidationError as e:
    print(f"ValidationError: {e}")

try:
    User(name=99, age=30)
except ValidationError as e:
    print(f"ValidationError: {e}")
Expected Output
User(name='Alice', age=30)
ValidationError: age: expected int, got str (value: 'thirty')
ValidationError: name: expected str, got int (value: 99)
Hints

Hint 1: Build a ModelMeta metaclass that reads __annotations__ and generates __init__ with runtime validation.

Hint 2: In the generated __init__, for each field call isinstance(value, expected_type) and raise ValidationError on failure.


#10Structural Type Checker at RuntimeHard
runtime-validationstructuralduck-typingProtocolinspect

Write structurally_satisfies(obj, protocol_cls) that checks at runtime whether an object has all the methods required by a Protocol class, with optional return-type annotation checking.

Python
import inspect
from typing import get_type_hints, List, Any


def structurally_satisfies(obj: Any, protocol_cls: type) -> tuple:
    """Returns (ok: bool, errors: List[str])"""
    errors: List[str] = []

    for name in dir(protocol_cls):
        if name.startswith("_"):
            continue
        proto_attr = getattr(protocol_cls, name, None)
        if not callable(proto_attr):
            continue
        obj_attr = getattr(obj, name, None)
        if obj_attr is None or not callable(obj_attr):
            errors.append(f"missing method: {name}")
            continue

        # Check return type annotation
        try:
            proto_hints = get_type_hints(proto_attr)
            obj_hints = get_type_hints(type(obj).__dict__.get(name, obj_attr))
            if "return" in proto_hints and "return" in obj_hints:
                if proto_hints["return"] != obj_hints["return"]:
                    errors.append(f"wrong return type for {name}")
        except Exception:
            pass

    return (len(errors) == 0, errors)


class Quackable:
    def quack(self) -> str: ...
    def waddle(self) -> None: ...


class Duck:
    def quack(self) -> str:
        return "Quack!"
    def waddle(self) -> None:
        pass


class Person:
    def quack(self) -> str:
        return "I quack like a duck"
    def waddle(self) -> None:
        pass


class Cat:
    def meow(self) -> str:
        return "Meow!"


for cls, label in [(Duck(), "Duck"), (Person(), "Person"), (Cat(), "Cat")]:
    ok, errs = structurally_satisfies(cls, Quackable)
    if ok:
        print(f"{label} satisfies Quackable: True")
    else:
        print(f"{label} satisfies Quackable: False — {errs[0]}")


class BadDuck:
    def quack(self) -> int:   # wrong return type
        return 1
    def waddle(self) -> None:
        pass


_, errs = structurally_satisfies(BadDuck(), Quackable)
missing_return = any("return" in e for e in errs)
print(f"Missing return type: {missing_return}")
Expected Output
Duck satisfies Quackable: True
Person satisfies Quackable: True
Cat satisfies Quackable: False — missing method: quack
Missing return type: True
Hints

Hint 1: inspect.getmembers(obj, callable) lists all callable attributes on the object.

Hint 2: Compare required method names against the object attributes. Optionally compare return type annotations.


#11Full Runtime Schema RegistryHard
runtime-validationschema-registryget_type_hintsdecorator

Build a SchemaRegistry with a @register_schema(name) decorator. The registry stores TypedDict classes and exposes a validate(name, data) method that returns errors.

Python
from typing import Dict, Any, List, get_type_hints, Type
try:
    from typing import TypedDict
except ImportError:
    from typing_extensions import TypedDict


class SchemaRegistry:
    def __init__(self) -> None:
        self._schemas: Dict[str, type] = {}

    def register(self, name: str):
        def decorator(cls: type) -> type:
            self._schemas[name] = cls
            return cls
        return decorator

    def describe(self, name: str) -> Dict[str, str]:
        schema = self._schemas[name]
        hints = get_type_hints(schema)
        return {k: getattr(v, "__name__", str(v)) for k, v in hints.items()}

    def validate(self, name: str, data: Dict[str, Any]) -> List[str]:
        if name not in self._schemas:
            return [f"Unknown schema: {name}"]
        schema = self._schemas[name]
        hints = get_type_hints(schema)
        errors: List[str] = []
        for key, expected_type in hints.items():
            if key not in data:
                errors.append(f"Missing key: {key}")
                continue
            if hasattr(expected_type, "__origin__"):
                continue
            value = data[key]
            if not isinstance(value, expected_type):
                errors.append(
                    f"{key}: expected {expected_type.__name__}, "
                    f"got {type(value).__name__}"
                )
        return errors


registry = SchemaRegistry()


@registry.register("user")
class UserSchema(TypedDict):
    name: str
    age: int
    email: str


print(f"Schema for 'user': {registry.describe('user')}")

valid = {"name": "Alice", "age": 30, "email": "[email protected]"}
invalid = {"name": "Bob", "age": "old"}

v_errors = registry.validate("user", valid)
i_errors = registry.validate("user", invalid)

print(f"Validate user {valid}: {'OK' if not v_errors else 'FAIL — ' + '; '.join(v_errors)}")
print(f"Validate user {invalid}: {'OK' if not i_errors else 'FAIL — ' + '; '.join(i_errors)}")
Expected Output
Schema for 'user': {'name': 'str', 'age': 'int', 'email': 'str'}
Validate user {'name': 'Alice', 'age': 30, 'email': '[email protected]'}: OK
Validate user {'name': 'Bob', 'age': 'old'}: FAIL — age: expected str, got int
Hints

Hint 1: A SchemaRegistry maps schema names to TypedDict classes. validate(name, data) looks up the schema and runs validate_typed_dict.

Hint 2: A @register_schema(name) decorator adds the class to the registry at definition time.

© 2026 EngineersOfAI. All rights reserved.