0

Relatively new to Python, I find myself having to evaluate a lot of arguments in functions/static methods and I was thinking if there is a user-friendly way to do this, so I ended up writing a function like so:

from typing import Any, Optional, Union

# Standard evaluator for default values
def eval_default(x: Any, default: Any, type: Optional[Any] = None) -> Any:
    """Helper function to set default value if None and basic type checking

    Args:
        x: the value to be evaluated
        default: the value to return if x is None
        type: the expected type of x, one of bool, int, float, str

    Raises:
        TypeError: for bool, int, float, str if x is not None and not of type

    Returns:
        x: either 'as is' if not None, or default if None, unless exception is raised.
    """
    # Infer type from default
    type = type(default) if type is None else type

    # Return default value if x is None, else check type and/or return x
    if x is None:
        return default
    elif not isinstance(x, type):
        if type == bool:
            raise TypeError("Variable can be either True or False.")
        elif type == int:
            raise TypeError("Variable must be an integer.")
        elif type == float:
            raise TypeError("Variable must be a float.")
        elif type == str:
            raise TypeError("Variable must be a string.")
    else:
        return x

Then in my main code I can do something like:


def some_method(self, some_variable:Optional[bool] = None) -> bool:
    """Some description
    """

    try:
        some_variable = eval_default(some_variable, False, bool)
    except TypeError:
        print("some_variable must be True or False.")

    return some_variable

Is there a simpler, more concise or more elegant standard practice to handle such a situation?

Many thanks in advance.

gkampolis
  • 31
  • 5
  • 1
    don't name it `type`, that is a builtin method – azro Dec 08 '21 at 20:04
  • 1
    Why in the definition of `some_method` do you not have `some_variable = False`? – defladamouse Dec 08 '21 at 20:12
  • @defladamouse see comment in your answer but boils down to keeping the same style with other default cases such as with lists, to avoid unexpected results because defaults are evaluated once during definition. – gkampolis Dec 08 '21 at 20:51
  • @Azro that’s a good point! Will amend code when in front of a laptop again tomorrow. Cheers! :) – gkampolis Dec 08 '21 at 20:52
  • 1
    Standard practice is to *not* perform this kind of runtime type checking. – chepner Dec 14 '21 at 16:17

2 Answers2

2

It looks to me like your eval_default function is just adding unnecessary complexity. If type checking is required, I'd just put it in the function, and declare the default as the actual default:

def some_method(self, some_variable:Optional[bool] = False) -> bool:
    """Some description
    """

    if not isinstance(some_variable, bool):
        print(f"some_variable must be True of False")

    return some_variable

Or if you need a mutable default:

def some_method(self, some_variable:Optional[list] = None) -> bool:
    """Some description
    """
    if some_variable is None:
        some_variable = []

    if not isinstance(some_variable, list):
        print(f"some_variable must be a list")

    return some_variable
defladamouse
  • 567
  • 2
  • 13
  • AFAIK default parameters in python are evaluated **once** during definition, which is one the common gotchas for unexpected behaviour (due to the mutability of the “default”, say for an empty list). The way around it is to have param=None in the function call and then assign the actual default value that you want. Better illustrated for example here: https://docs.python-guide.org/writing/gotchas/ – gkampolis Dec 08 '21 at 20:49
  • Yes, mutable defaults are an issue in function definition, but the options covered in your `default_eval` function would all be fine. I've added an example with `list` to cover mutable defaults as well. – defladamouse Dec 09 '21 at 08:18
  • Thanks, that cleared it up for me. I'll just do a ternary `some_var = default if some_var is None else some_var` for the default value to have consistency between immutables and mutables and check in the next line about type, avoiding eval_default entirely. – gkampolis Dec 09 '21 at 10:54
1

Your eval_default doesn't do anything to narrow the type from Optional[bool] to bool:

def some_method(self, some_variable:Optional[bool] = None) -> bool:
    try:
        some_variable = eval_default(some_variable, False, bool)
    except TypeError:
        print("some_variable must be True or False.")

    reveal_type(some_variable)  # note: Revealed type is "Union[builtins.bool, None]"

It potentially could with careful use of generics, but you'd need to assign the return value to a different name, which only makes it harder to use.

My usual pattern for de-Optional-ing default-None args is:

def some_method(self, some_variable:Optional[bool] = None) -> bool:
    some_variable = some_variable or False
    reveal_type(some_variable)  # note: Revealed type is "builtins.bool"

Of course for a bool (or any immutable type) you could just do:

def some_method(self, some_variable: bool = False) -> bool:
    reveal_type(some_variable)  # note: Revealed type is "builtins.bool"

which avoids the whole problem! The optional_arg or default trick is really only useful for cases where the default is something you're going to mutate and you need to construct a new object per function call (e.g. you want to initialize the default to a new empty list so you can append items to it).

Samwise
  • 68,105
  • 3
  • 30
  • 44
  • So in the general case (I get now that's not needed with bool but as an example of a pattern) I could do: ```python def some_method(self, some_variable:Optional[bool[ = None) -> bool: some_variable = False if some_variable is None else some_variable # continue with method logic from here ``` The `some_variable = some_variable or False` is jsut the pattern of `var = var or default_value`, relying on None evaluating to False? – gkampolis Dec 09 '21 at 10:02
  • Ah, I now see that comments don't keep the layout neatly. Here is a picture of the above: [](https://i.ibb.co/3f2651Y/image.png) – gkampolis Dec 09 '21 at 10:10
  • 1
    Yup. Using the simple `or` is easier to read IMO but the effect is the same. – Samwise Dec 09 '21 at 15:39