7

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?

Community
  • 1
  • 1
S. Gamgee
  • 481
  • 5
  • 14
  • 1
    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 Jul 28 '16 at 14:06
  • 2
    Maybe this will help you: http://stackoverflow.com/questions/15299878/how-to-use-python-decorators-to-check-function-arguments – Gábor Fekete Jul 28 '16 at 14:08
  • 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 Jul 28 '16 at 14:20
  • "This parameter" is ambiguous if there is more than one.... – Ryan McCullagh Jul 28 '16 at 14:23
  • 1
    @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 Jul 28 '16 at 14:31
  • @Daenyth Fair enough. – S. Gamgee Jul 28 '16 at 14:38
  • Since no Python version is specified, it may be worth mentioning function annotations in 3+ as well (see https://pypi.python.org/pypi/typecheck-decorator). – kloffy Jul 28 '16 at 15:14

3 Answers3

4

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
VoteCoffee
  • 3,300
  • 26
  • 31
tobias_k
  • 74,298
  • 11
  • 102
  • 155
  • Very detailed and thorough answer! And very promptly delivered, I might add! Clap and a half to you! – S. Gamgee Jul 28 '16 at 14:29
  • Per the documentation, ValueError is to be used when the Type is correct but the value is not with an acceptable range. You should be using TypeError instead https://docs.python.org/3/library/exceptions.html#ValueError https://docs.python.org/3/library/exceptions.html#TypeError I edited your answer to reflect this. – VoteCoffee Feb 15 '21 at 11:11
1

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'>
galaxyan
  • 5,119
  • 2
  • 15
  • 36
1

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

perfecto25
  • 488
  • 4
  • 11
  • I hate that python is missing logic for parameter type and range checking, but at the same time unfortunately, it's pythonic not to check those things as in python the onus is on the code to document the function and on the user of functions to be aware of their expected usage. Python also follows duck typing and is not meant to be strict. That's not very helpful when you're layers deep in logic of course. https://realpython.com/lessons/duck-typing/ – VoteCoffee Feb 15 '21 at 11:21