15  Function (Validation)

15.1 My Arguments Validation

I would use these balanced approach:

from typing import Literal

def fn1(a: Literal["apple", "banana", "orange"]):

    valid_fruits = frozenset({"apple", "banana", "orange"})

    if a not in valid_fruits:
        raise ValueError(f"Invalid value: {a}. Must be one of: {', '.join(valid_fruits)}")
    # Rest of the function
    return a
fn1("apple")
'apple'
try:
    fn1("watermelon")
except ValueError as e:
    print(e)
Invalid value: watermelon. Must be one of: orange, apple, banana

15.2 Arguments Validation (Approach)

Q: If I have a function, say, fn1(a), which have an argument a with only 3 possible text values: “apple”, “banana”, “orange”.

Give me different approach to perform arguments validation.

I’ll help you explore different approaches to validate function arguments when there are only specific allowed values. Let’s examine various methods, starting from simple to more sophisticated approaches.

15.2.1 If-else statements (Basic Approach):

def fn1(a):
    if a not in ["apple", "banana", "orange"]:
        raise ValueError(f"Invalid value: {a}. Must be one of: apple, banana, orange")
    # Rest of the function
    return a

15.2.2 Using an Enum class (Type Safety)

from enum import Enum, auto

class Fruit(Enum):
    APPLE = "apple"
    BANANA = "banana"
    ORANGE = "orange"

def fn1(a: Fruit) -> str:
    # The type hint ensures only Fruit enum values are accepted
    return a.value

15.2.3 Using a Set for faster lookup:

VALID_FRUITS = frozenset({"apple", "banana", "orange"})

def fn1(a: str) -> str:
    if a not in VALID_FRUITS:
        raise ValueError(f"Invalid value: {a}. Must be one of: {', '.join(VALID_FRUITS)}")
    return a

15.2.4 Using decorators (Advanced):

from functools import wraps
from typing import Callable, Any

def validate_fruit(func: Callable) -> Callable:
    valid_fruits = frozenset({"apple", "banana", "orange"})
    
    @wraps(func)
    def wrapper(a: str, *args: Any, **kwargs: Any) -> Any:
        if a not in valid_fruits:
            raise ValueError(f"Invalid value: {a}. Must be one of: {', '.join(valid_fruits)}")
        return func(a, *args, **kwargs)
    
    return wrapper

@validate_fruit
def fn1(a: str) -> str:
    return a
try:
    fn1("watermelon")
except ValueError as e:
    print(e)
Invalid value: watermelon. Must be one of: orange, apple, banana

15.2.5 Using Literal type hints (Python 3.8+):

from typing import Literal

def fn1(a: Literal["apple", "banana", "orange"]) -> str:
    return a
fn1("watermelon")
'watermelon'

Let me explain the pros and cons of each approach:

The if-else approach is straightforward and easy to understand, but it’s verbose if you need to reuse the validation in multiple functions.

The Enum approach provides type safety and IDE support, making it harder to make mistakes. It’s especially useful in larger applications where you want to ensure type consistency. However, it requires more setup code.

The Set approach offers faster lookup times compared to lists and is memory efficient. It’s a good balance between simplicity and performance, especially when you have many valid values.

The decorator approach allows you to separate the validation logic from the function implementation, making it reusable across multiple functions. However, it’s more complex and might be overkill for simple cases.

The Literal type hints provide static type checking if you use a type checker like mypy, but they don’t enforce runtime validation by themselves. They’re great for documentation and catching errors during development.

For your specific case, since you mentioned you’re at an intermediate Python level, I would recommend either the Set approach or the Enum approach:

  • Use the Set approach if you want something simple and performant
  • Use the Enum approach if you want better type safety and IDE support

Would you like me to elaborate on any of these approaches or show you how to combine them for more robust validation?