Skip to main content

How to Validate Function Parameter Rules in Python

Ensuring that function parameters meet specific criteria (types, ranges, or formats) is the first line of defense against runtime errors and logical bugs. While Python is dynamically typed, robust applications require strict validation logic to guarantee data integrity.

This guide explores native methods for validating parameters using type checking and conditional logic, as well as advanced techniques using decorators and modern libraries like Pydantic.

Basic Type Validation (isinstance)

The most fundamental validation is ensuring an argument is of the expected data type. While Python provides Type Hints (def func(x: int)), these are ignored at runtime. To enforce them, you must use isinstance().

Implementing Type Checks

Use isinstance(value, type) to check types. It supports tuples for checking multiple allowed types (e.g., (int, float)).

def calculate_area(length, width):
# ⛔️ Incorrect: No validation. Passing strings will crash or produce weird output.
# return length * width

# ✅ Correct: Validate types explicitly
if not isinstance(length, (int, float)) or not isinstance(width, (int, float)):
raise TypeError(f"Inputs must be numbers. Got {type(length)} and {type(width)}")

return length * width

# Test cases
print(f"Area: {calculate_area(10, 5)}")

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

Output:

Area: 50
Error: Inputs must be numbers. Got <class 'str'> and <class 'int'>
note

Always prefer isinstance(obj, type) over type(obj) == type because isinstance supports inheritance (subclasses are considered valid).

Range and Value Validation

Once the type is confirmed, you often need to ensure the value falls within a logical range (e.g., age cannot be negative).

Logic Checks

Use standard comparison operators (<, >, <=, >=) and raise a ValueError if the data violates business rules.

def set_age(age):
# 1. Type Check
if not isinstance(age, int):
raise TypeError("Age must be an integer")

# 2. Range Check
# ✅ Correct: Ensure age is realistic
if not (0 <= age <= 120):
raise ValueError(f"Age {age} is invalid. Must be between 0 and 120.")

return f"Age set to {age}"

try:
print(set_age(25))
print(set_age(150))
except ValueError as e:
print(f"Validation Failed: {e}")

Output:

Age set to 25
Validation Failed: Age 150 is invalid. Must be between 0 and 120.

Pattern Matching (Regex)

For strings, validation often involves checking patterns (emails, phone numbers) using the re module.

import re

def validate_email(email):
# Simple regex for demonstration
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'

if not re.match(pattern, email):
raise ValueError("Invalid email format")

return True

try:
validate_email("user@labex")
except ValueError as e:
print(f"Error: {e}")

Output:

Error: Invalid email format

Advanced: Reusable Validation with Decorators

If you find yourself repeating the same checks (e.g., "input must not be None" or "input must be positive") across many functions, utilize Decorators. This keeps your core function logic clean.

Creating a Validation Decorator

from functools import wraps

def validate_positive(func):
"""Decorator to ensure all numeric arguments are positive."""
@wraps(func)
def wrapper(*args, **kwargs):
for arg in args:
if isinstance(arg, (int, float)) and arg < 0:
raise ValueError(f"Negative argument found: {arg}")
return func(*args, **kwargs)
return wrapper

@validate_positive
def calculate_sqrt(x):
return x ** 0.5

try:
print(f"Root: {calculate_sqrt(16)}")
print(f"Root: {calculate_sqrt(-9)}")
except ValueError as e:
print(f"Decorator caught error: {e}")

Output:

Root: 4.0
Decorator caught error: Negative argument found: -9

Modern Approach: Using Pydantic

For complex applications, writing manual if statements for every parameter is inefficient. The Pydantic library is the industry standard for parsing and validation in modern Python.

It forces types at runtime and provides detailed error messages automatically.

from pydantic import BaseModel, Field, ValidationError

class UserProfile(BaseModel):
username: str
# 'gt' = greater than, 'le' = less than or equal
age: int = Field(gt=0, le=120)
email: str

def create_user(data: dict):
try:
# ✅ Correct: Pydantic validates type, existence, and constraints
user = UserProfile(**data)
print(f"User created: {user.username}")
except ValidationError as e:
print("Validation Error:")
print(e)

# Valid Input
create_user({"username": "Alice", "age": 30, "email": "alice@example.com"})

# Invalid Input (String age, invalid range)
create_user({"username": "Bob", "age": "too old", "email": "bob@example.com"})

Output:

User created: Alice
Validation Error:
1 validation error for UserProfile
age
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='too old', input_type=str]
tip

Pydantic handles type coercion automatically where safe (e.g., converting string "30" to integer 30), but raises errors for incompatible types.

Conclusion

To validate function parameters effectively:

  1. Use isinstance for basic type safety to prevent AttributeError or TypeError deep in your code.
  2. Raise ValueError for logical constraints (ranges, patterns).
  3. Use Decorators to abstract repetitive checks (like non-null or positive numbers).
  4. Adopt Pydantic for complex data structures to save time and reduce boilerplate code.