Skip to content

How to Enforce Type Hints at Runtime in Python

Learn how to validate Python type hints at runtime using manual checks, decorators, or libraries like typeguard and pydantic to catch type mismatches early.

In Python, type hints (def func(x: int) -> str:) are, by default, purely for static analysis tools like mypy or IDEs. The Python interpreter completely ignores them at runtime, meaning you can pass a string to a function expecting an int without an immediate crash (until the logic breaks).

To make Python behave more like a statically typed language at runtime, you need to enforce these hints explicitly. This guide explores manual validation, decorators, and libraries like typeguard and pydantic.

Understanding the Problem

Normally, Python allows this:

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

print(add("1", "2"))

Output:

12

To prevent this behavior and raise an error immediately when types mismatch, we must implement runtime checks.

Method 1: Manual Checks (isinstance)

The simplest way is to manually validate inputs at the start of your function. This is verbose but requires no external dependencies.

def add_strict(a: int, b: int) -> int:
    if not isinstance(a, int):
        raise TypeError(f"Argument 'a' must be int, not {type(a).__name__}")
    if not isinstance(b, int):
        raise TypeError(f"Argument 'b' must be int, not {type(b).__name__}")
        
    return a + b

try:
    add_strict(1, "2")
except TypeError as e:
    print(e)

Output:

Argument 'b' must be int, not str

Method 2: Using Decorators for Enforcement

You can automate validation by writing a decorator that inspects the function's type hints (__annotations__) and checks incoming arguments against them.

import functools
import inspect

def enforce_types(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        hints = inspect.get_annotations(func)
        bound_args = inspect.signature(func).bind(*args, **kwargs)
        bound_args.apply_defaults()
        
        for name, value in bound_args.arguments.items():
            if name in hints:
                expected_type = hints[name]
                if not isinstance(value, expected_type):
                    raise TypeError(f"Argument '{name}' expected {expected_type}, got {type(value)}")
        
        return func(*args, **kwargs)
    return wrapper

@enforce_types
def multiply(a: int, b: int):
    return a * b

print(multiply(5, 5))

try:
    print(multiply(5, "5"))
except TypeError as e:
    print(f"Error caught: {e}")

Output:

25
Error caught: Argument 'b' expected <class 'int'>, got <class 'str'>

For data classes, APIs, or configuration, pydantic is the industry standard. It not only checks types but attempts to coerce valid data (e.g., converts "5" to 5 if expecting an int) and provides detailed error reports.

Installation:

pip install pydantic
from pydantic import BaseModel, ValidationError

class User(BaseModel):
    id: int
    username: str
    is_active: bool = True

user = User(id=1, username="Alice")
print(user)

user_coerced = User(id="10", username="Bob")
print(user_coerced)

try:
    User(id="abc", username="Charlie")
except ValidationError as e:
    print("Validation Error:", e)

Output:

id=1 username='Alice' is_active=True
id=10 username='Bob' is_active=True
Validation Error: 1 validation error for User
id
  Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='abc', input_type=str]
    For further information visit https://errors.pydantic.dev/2.10/v/int_parsing

Conclusion

To enforce Python type hints at runtime:

  1. Use Manual Assertions for simple scripts with few parameters.
  2. Use Decorators (typeguard or custom) to enforce function signatures automatically.
  3. Use Pydantic for robust data validation, especially when handling external data (JSON, APIs).
Navigation