7
votes

I am attempting to write exception raising code blocks into my Python code in order to ensure that the parameters passed to the function meet appropriate conditions (i.e. making parameters mandatory, type-checking parameters, establishing boundary values for parameters, etc...). I understand satisfactorily how to manually raise exceptions as well as handling them.

from numbers import Number

def foo(self, param1 = None, param2 = 0.0, param3 = 1.0):
   if (param1 == None):
      raise ValueError('This parameter is mandatory')
   elif (not isinstance(param2, Number)):
      raise ValueError('This parameter must be a valid Numerical value')
   elif (param3 <= 0.0):
      raise ValueError('This parameter must be a Positive Number')
   ...

This is an acceptable (tried and true) way of parameter checking in Python, but I have to wonder: Since Python does not have a way of writing Switch-cases besides if-then-else statements, is there a more efficient or proper way to perform this task? Or is implementing long stretches of if-then-else statements my only option?

3
You could create a function decorator, something like @check(types=[None, None, float, float], ranges=[None,None,(0.0,10.0),None]) (here, None meaning "no restriction") - tobias_k
Believe it or not, I think that using assert statements might be more beneficial to me than using if-elif statements. I actually got the idea from both examining the link posted in the comments as well as the code used for the decorator. So thanks, guys! - S. Gamgee
"This parameter" is ambiguous if there is more than one.... - Ryan
@S.Gamgee assert statements don't execute in optimized code; this is fine for internal sanity checks but if the api is intended to be reused you should use ValueErrors - Daenyth

3 Answers

4
votes

You could create a decorator function and pass the expected types and (optional) ranges as parameters. Something like this:

def typecheck(types, ranges=None):
    def __f(f):
        def _f(*args, **kwargs):
            for a, t in zip(args, types):
                if not isinstance(a, t):
                    raise TypeError("Expected %s got %r" % (t, a))
            for a, r in zip(args, ranges or []):
                if r and not r[0] <= a <= r[1]:
                    raise ValueError("Should be in range %r: %r" % (r, a))
            return f(*args, **kwargs)
        return _f
    return __f

Instead of if ...: raise you could also invert the conditions and use assert, but as noted in comments those might not always be executed. You could also extend this to allow e.g. open ranges (like (0., None)) or to accept arbitrary (lambda) functions for more specific checks.

Example:

@typecheck(types=[int, float, str], ranges=[None, (0.0, 1.0), ("a", "f")])
def foo(x, y, z):
    print("called foo with ", x, y, z)
    
foo(10, .5, "b")  # called foo with  10 0.5 b
foo([1,2,3], .5, "b")  # TypeError: Expected <class 'int'>, got [1, 2, 3]
foo(1, 2.,"e")  # ValueError: Should be in range (0.0, 1.0): 2.0
1
votes

I think you can use decorator to check the parameters.

def parameterChecker(input,output):
...     def wrapper(f):
...         assert len(input) == f.func_code.co_argcount
...         def newfun(*args, **kwds):
...             for (a, t) in zip(args, input):
...                 assert isinstance(a, t), "arg {} need to match {}".format(a,t)
...             res =  f(*args, **kwds)
...             if not isinstance(res,collections.Iterable):
...                 res = [res]
...             for (r, t) in zip(res, output):
...                 assert isinstance(r, t), "output {} need to match {}".format(r,t)   
...             return f(*args, **kwds)
...         newfun.func_name = f.func_name
...         return newfun
...     return wrapper

example:
@parameterChecker((int,int),(int,))
... def func(arg1, arg2):
...     return '1'
func(1,2)
AssertionError: output 1 need to match <type 'int'>

func(1,'e')
AssertionError: arg e need to match <type 'int'>
1
votes

This has been bugging me for a while about Python, there is no standard way to output if a provided param is None or has missing value, nor handle a JSON/Dict object gracefully,

for example I want to output the actual parameter name in the error message,

username = None

if not username:
    log.error("some parameter is missing value")

There is no way to pass the actual parameter name, unless you do this artificially and messily by hard coding the parameter in the error output message, ie,

if not username:
    log.error("username is missing value")

but this is both messy and prone to syntax errors, and pain in butt to maintain.

For this reason, I wrote up a "Dictator" function,

https://medium.com/@mike.reider/python-dictionaries-get-nested-value-the-sane-way-4052ab99356b

If you add your parameters into a dict, or read your parameters from a YAML or JSON config file, you can tell Dictator to raise a ValueError if a param is null,

for example,

config.yaml

skills:
  sports:
    - hockey
    - baseball

now your py program reads in this config file and the parameters, as a JSON dict,

with open(conf_file, 'r') as f:
    config = yaml.load(f)

now set your parameters and also check if theyre NULL

sports = dictator(config, "skills.sports", checknone=True)

if sports is None, it will raise a ValueError, telling you exactly what parameter is missing

ValueError("missing value for ['skills']['sports']")

you can also provide a fallback value to your parameter, so in case it is None, give it a default fallback value,

sports = dictator(config, "skills.sports", default="No sports found")

This avoids ugly Index/Value/Key error exceptions.

Its a flexible, graceful way to handle large dictionary data structures and also gives you the ability to check your program's parameters for Null values and output the actual parameter names in the error message