Parameter validation, Best practices in Python

even though already answered, this is too long for a comment, so i'll just add an another answer.

In general, type checking is done for two reasons: making sure your function actually completes, and avoiding difficult-to-debug downstream failures from bad output.

For the first problem, the answer is always appropriate - EAFP is the normal method. and you don't worry about bad inputs.

For the second... the answer depends on your normal use cases, and you DO worry about bad inputs/bugs. EAFP is still appropriate (and it's easier, and more debuggable) when bad inputs always generate exceptions (where 'bad inputs' can be limited to the types of bad inputs that your app expects to produce, possibly). But if there's a possibility bad inputs could create a valid output, then LYBL may make your life easier later.

Example: let's say that you call square(), put this value into a dictionary, and then (much) later extract this value from the dictionary and use it as an index. Indexes, of course, must be integers.

square(2) == 4, and is a valid integer, and so is correct. square('a') will always fail, because 'a'*'a' is invalid, and will always throw an exception. If these are the only two possibilities, then you can safely use EAFP. if you do get bad data, it will throw an exception, generate a traceback, and you can restart with pdb and get a good indication of what's wrong.

however... let's say that your app uses some FP. and it's possible (assuming you have a bug! not normal operation of course) for you to accidentally call square(1.43). This will return a valid value - 2.0449 or so. you will NOT get exception here, and so your app will happily take that 2.0449 and put it in the dictionary for you. Much later, your app will pull this value back out of the dictionary, use it as an index into a list and - crash. You'll get a traceback, you'll restart with pdb, and realize that doesn't help you at all, because that value was calculated a long time ago, and you no longer have the inputs, or any idea how that data got there. And those are not fun to debug.

In those cases, you can use asserts (special form of LYBL) to move detection of those sorts of bugs earlier, or you can do it explicitly. If you don't ever have a bug calling that function, then either one will work. But if you do... then you'll really be glad you checked the inputs artificially close to the failure, rather than naturally some random later place in your app.


As mentioned by the documentation here, Python follows an EAFP approach. This means that we usually use more try and catch blocks instead of trying to validate parameters. Let me demonstrate:

import os


def get_abs_directory(path):
    try:
        if os.path.isdir(path):
            return path
        else:
            return os.path.split(os.path.abspath(path))[0]
    except TypeError:
        print "You inserted the wrong type!"


if __name__ == '__main__':
    get_abs_directory(1)  # Using an int instead of a string, which is caught by TypeError

You could however, wish to code in a LBYL (Look Before You Leap) style and this would look something like this:

import os


def get_abs_directory(path):

    if not isinstance(path, str):
        print "You gave us the wrong type, you big meany!"
        return None

    if os.path.isdir(path):
        return path
    else:
        return os.path.split(os.path.abspath(path))[0]

if __name__ == '__main__':
    get_abs_directory(1)

EAFP is the de-facto standard in Python for situations like this and at the same time, nothing impedes you from following LBYL all the way if you like to.

However, there are reservations when EAFP is applicable:

  • When the code is still able to deal with exception scenarios, break at some point or enable the caller to validate possible errors then it may be best just to follow the EAFP principle.

  • When using EAFP leads to silent errors then explicit checks/validations (LBYL) may be best.

Regarding that, there is a Python module, parameters-validation, to ease validation of function parameters when you need to:

@validate_parameters
def register(
    token: strongly_typed(AuthToken),
    name: non_blank(str),
    age: non_negative(int),
    nickname: no_whitespaces(non_empty(str)),
    bio: str,
):
    # do register

Disclaimer: I am the project maintainer.

Tags:

Python