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:
12To 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 strMethod 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'>Method 3: Using Pydantic (Recommended for Data)
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 pydanticfrom 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_parsingConclusion
To enforce Python type hints at runtime:
- Use Manual Assertions for simple scripts with few parameters.
- Use Decorators (
typeguardor custom) to enforce function signatures automatically. - Use Pydantic for robust data validation, especially when handling external data (JSON, APIs).