Skip to main content

How to Enforce Type Hints at Runtime in Python

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

# This runs without error, returning "12" (concatenation)
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:
# ✅ Correct: Validate inputs manually
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):
# Get type hints
hints = inspect.get_annotations(func)
# Bind arguments to parameter names
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]
# Check type (supports simple types like int, str)
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

# ✅ Correct: Standard usage
print(multiply(5, 5))

# ⛔️ Incorrect: Raises TypeError automatically
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'>
warning

This simple decorator works for basic types (int, str) but fails on complex generics like List[int] or Union[int, str]. For complex types, rely on Method 3 or use the typeguard library.

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

# ✅ Correct: Valid Input
user = User(id=1, username="Alice")
print(user)

# ✅ Correct: Type Coercion (string "10" becomes int 10)
user_coerced = User(id="10", username="Bob")
print(user_coerced)

# ⛔️ Incorrect: Invalid Input that cannot be 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
tip

Pydantic also offers a @validate_call decorator (in v2) to enforce types on standard functions, similar to the custom decorator in Method 2 but much more powerful.

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