How to validate function arguments¶
This guide covers practical patterns for checking and validating the arguments passed to your functions. Proper validation catches errors early, produces clear error messages, and makes your functions safer to use.
Type checking with isinstance()¶
Use isinstance() to verify that an argument is the expected type. Raise a TypeError with a clear message when the check fails.
def calculate_area(width: float, height: float) -> float:
"""Calculate the area of a rectangle."""
if not isinstance(width, (int, float)):
raise TypeError(f"width must be a number, got {type(width).__name__}")
if not isinstance(height, (int, float)):
raise TypeError(f"height must be a number, got {type(height).__name__}")
return width * height
print(calculate_area(5, 3))
print(calculate_area(2.5, 4.0))
try:
calculate_area("5", 3)
except TypeError as exc:
print(f"TypeError: {exc}")
You can check for multiple types at once by passing a tuple to isinstance(). This is useful when a parameter accepts both int and float, or both str and bytes.
Value range validation¶
After confirming the type is correct, check that the value falls within an acceptable range. Raise a ValueError for values of the right type but wrong content.
def set_volume(level: int) -> str:
"""Set the volume to a level between 0 and 100."""
if not isinstance(level, int):
raise TypeError(f"level must be an integer, got {type(level).__name__}")
if not 0 <= level <= 100:
raise ValueError(f"level must be between 0 and 100, got {level}")
return f"Volume set to {level}"
print(set_volume(75))
try:
set_volume(150)
except ValueError as exc:
print(f"ValueError: {exc}")
try:
set_volume(-10)
except ValueError as exc:
print(f"ValueError: {exc}")
def create_discount(percentage: float) -> str:
"""Create a discount with a percentage between 0 and 100."""
if not isinstance(percentage, (int, float)):
raise TypeError(
f"percentage must be a number, got {type(percentage).__name__}"
)
if percentage < 0 or percentage > 100:
raise ValueError(
f"percentage must be between 0 and 100, got {percentage}"
)
return f"Discount of {percentage}% applied"
print(create_discount(15))
print(create_discount(0))
try:
create_discount(120)
except ValueError as exc:
print(f"ValueError: {exc}")
String format validation¶
For string arguments, you may need to check the format matches a required pattern. Use simple string methods for basic checks and re for more complex patterns.
import re
def send_message(email: str, message: str) -> str:
"""Send a message to an email address."""
if not isinstance(email, str):
raise TypeError(f"email must be a string, got {type(email).__name__}")
if not isinstance(message, str):
raise TypeError(f"message must be a string, got {type(message).__name__}")
# Basic email format check
email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
if not re.match(email_pattern, email):
raise ValueError(f"invalid email address: {email!r}")
if len(message.strip()) == 0:
raise ValueError("message must not be empty")
return f"Message sent to {email}"
print(send_message("alice@example.com", "Hello, Alice!"))
try:
send_message("not-an-email", "Hello")
except ValueError as exc:
print(f"ValueError: {exc}")
try:
send_message("alice@example.com", " ")
except ValueError as exc:
print(f"ValueError: {exc}")
Raising appropriate exceptions¶
Choose the correct exception type to communicate what went wrong:
TypeError— the argument is the wrong type (for example, a string where an integer is expected)ValueError— the argument is the right type but has an invalid value (for example, a negative number where only positive numbers are accepted)
Always include a message that describes what was expected and what was received.
def register_user(username: str, age: int) -> dict:
"""Register a new user with validation."""
# Type checks
if not isinstance(username, str):
raise TypeError(
f"username must be a string, got {type(username).__name__}"
)
if not isinstance(age, int):
raise TypeError(
f"age must be an integer, got {type(age).__name__}"
)
# Value checks
if len(username) < 3:
raise ValueError(
f"username must be at least 3 characters, got {len(username)}"
)
if age < 0 or age > 150:
raise ValueError(
f"age must be between 0 and 150, got {age}"
)
return {"username": username, "age": age}
print(register_user("alice", 30))
try:
register_user("ab", 25)
except ValueError as exc:
print(f"ValueError: {exc}")
try:
register_user(123, 25)
except TypeError as exc:
print(f"TypeError: {exc}")
Assertions versus exceptions¶
Use exceptions for validating arguments in public-facing functions. Use assertions for internal consistency checks that should never fail if the code is correct.
The critical difference: assertions can be disabled with the -O (optimise) flag, so they must not be relied upon for input validation.
def divide(numerator: float, denominator: float) -> float:
"""Divide two numbers.
This is a public function, so use exceptions for validation.
"""
if not isinstance(numerator, (int, float)):
raise TypeError(f"numerator must be a number, got {type(numerator).__name__}")
if not isinstance(denominator, (int, float)):
raise TypeError(f"denominator must be a number, got {type(denominator).__name__}")
if denominator == 0:
raise ValueError("denominator must not be zero")
return numerator / denominator
def _normalise_weights(weights: list[float]) -> list[float]:
"""Normalise weights so they sum to 1.
This is an internal function. The assertion guards against
programming errors, not user input.
"""
total = sum(weights)
assert total > 0, "internal error: weights must sum to a positive number"
return [w / total for w in weights]
print(divide(10, 3))
print(_normalise_weights([2.0, 3.0, 5.0]))
Creating reusable validation decorators¶
When you have multiple functions that need the same validation logic, write a decorator to avoid repeating yourself.
import functools
import inspect
def validate_types(**expected_types):
"""Decorator factory that validates argument types."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Bind arguments to parameter names
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
for param_name, expected_type in expected_types.items():
if param_name in bound.arguments:
value = bound.arguments[param_name]
if not isinstance(value, expected_type):
raise TypeError(
f"{param_name} must be {expected_type.__name__}, "
f"got {type(value).__name__}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@validate_types(name=str, age=int)
def greet_user(name: str, age: int) -> str:
return f"Hello, {name}! You are {age} years old."
print(greet_user("Alice", 30))
try:
greet_user(123, 30)
except TypeError as exc:
print(f"TypeError: {exc}")
def validate_positive(*param_names: str):
"""Decorator factory that ensures specified parameters are positive numbers."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
for param_name in param_names:
if param_name in bound.arguments:
value = bound.arguments[param_name]
if not isinstance(value, (int, float)):
raise TypeError(
f"{param_name} must be a number, "
f"got {type(value).__name__}"
)
if value <= 0:
raise ValueError(
f"{param_name} must be positive, got {value}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@validate_positive("width", "height")
def rectangle_area(width: float, height: float) -> float:
"""Calculate the area of a rectangle."""
return width * height
print(rectangle_area(5, 3))
try:
rectangle_area(-2, 3)
except ValueError as exc:
print(f"ValueError: {exc}")
try:
rectangle_area("five", 3)
except TypeError as exc:
print(f"TypeError: {exc}")
Combining multiple validations¶
You can stack multiple validation decorators on a single function. They execute from bottom to top (closest to the function first).
@validate_types(name=str, quantity=int, price=float)
@validate_positive("quantity", "price")
def create_order(name: str, quantity: int, price: float) -> dict:
"""Create an order with validated arguments."""
return {
"name": name,
"quantity": quantity,
"price": price,
"total": quantity * price,
}
print(create_order("Widget", 3, 9.99))
try:
create_order("Widget", -1, 9.99)
except ValueError as exc:
print(f"ValueError: {exc}")
Summary¶
The key patterns for validating function arguments are as follows:
- Use
isinstance()to check argument types and raiseTypeErrorwhen they do not match - Validate value ranges and raise
ValueErrorfor values of the correct type but invalid content - Use
re.match()for string format validation - Use exceptions for public API validation and assertions for internal consistency checks
- Write reusable validation decorators to avoid repeating the same checks across multiple functions