[Python-ideas] Repurpose `assert' into a general-purpose check

Steven D'Aprano steve at pearwood.info
Tue Jan 16 20:42:47 EST 2018


On Tue, Jan 16, 2018 at 07:37:29AM -0800, smarie wrote:

[...]
> > The problem with a statement called "validate" is that it will break a 
> > huge number of programs that already include functions and methods using 
> > that name.
> >
> 
> You definitely make a point here. But that would be the case for absolutely 
> *any* language evolution as soon as the proposed statements are plain old 
> english words. Should it be a show-stopper ? I dont think so.

It is not a show-stopper, but it is a very, very larger barrier to 
adding new keywords. If there is a solution to the problem that doesn't 
require a new keyword, that is almost always preferred over breaking 
people's code when they upgrade.


> > But apart from the use of a keyword, we already have a way to do almost 
> > exactly what you want:
> >
> >     if not expression: raise ValidationError(message)
> >
> > after defining some appropriate ValidationError class. And it is only a 
> > few key presses longer than the proposed:
> >
> >     validate expression, ValidationError, message
> >
> 
> This is precisely what is not good in my opinion: here you do not separate 
> <validation means> from <validation intent>. Of course if <validation 
> means> is just a "x > 0" statement, it works, but now what if you rely on a 
> 3d-party provided validation function (or even yours) such as e.g. 
> "is_foo_compliant" ? 

There's no need to invent a third-party validation function. It might be 
my own validation function, or it might be a simple statement like:

    if variable is None: ...

which can fail with NameError if "variable" is not defined. Or "x > 0" 
can fail if x is not a number.

Regardless of what the validation check does, there are two ways it can 
not pass:

- the check can fail;

- or the check can raise an exception.

The second generally means that the check code itself is buggy or 
incomplete, which is why unittest reports these categories separately. 


That is a good thing, not a problem to be fixed. For example:


    # if x < 0: raise ValueError('x must not be negative')
    validate x >= 0, ValueError, 'x must not be negative'


Somehow my code passes a string to this as x. Wouldn't you, the 
developer, want to know that there is a code path that somehow results 
in x being a string? I know I would. Maybe that will become obvious 
later on, but it is best to determine errors as close to their source as 
we can.

With your proposed validate keyword, the interpreter lies to me: it says 
that the check x >= 0 *fails* rather than raises, which implies that x 
is a negative number. Now I waste my time trying to debug how x could 
possibly be a negative number when the symptom is actually very 
different (x is a string).

Hiding the exception is normally a bad thing, but if I really want to do that, I can write a helper 
function:

def is_larger_or_equal(x, y):
    try:
        return x >= y
    except:
        return False


If I find myself writing lots of such helper functions, that's probably 
a hint that I am hiding too much information. Bare excepts have been 
called the most diabolic Python anti-pattern:

https://realpython.com/blog/python/the-most-diabolical-python-antipattern/

so hiding exceptions *by default* (as your proposed validate statement 
would do) is probably not a great idea.

The bottom line is, if my check raises an exception instead of passing 
or failing, I want to know that it raised. I don't want the error to 
be hidden as a failed check.


> if not is_foo_compliant(x): raise ValidationError(message)
> 
> What if this third part method raises an exception instead of returning 
> False in some cases ? 

Great! I would hope it did raise an exception if it were passed 
something that it wasn't expecting and can't deal with.

There may be some cases where I want a validation function to ignore all 
errors, but if so, I will handle them individually with a wrapper 
function, which let's me decide how to handle individual errors:

def my_foo_compliant(x):
    try:
        return is_foo_compliant(x)
    except SpamError, EggsError:
        return True
    except CheeseError:
        return False
    except:
        raise


But I can count the number of times I've done that in practice on the 
fingers of one hand.

[...]
> The goal is really to let developers express their applicative intent 
> =(what should be checked and what is the outcome if anything goes wrong), 
> and give them confidence that the statement will always fail the same way,
> whatever the failure modes /behaviour of the checkers used in the 
> statement.

I don't agree that this is a useful goal for the Python interpreter to 
support as a keyword or built-in function.

If you want to create your own library to do this, I wish you good luck, 
but I would not use it and I honestly think that it is a trap: something 
that seems to be convenient and useful but actually makes maintaining 
code harder by hiding unexpected, unhandled cases as if they were 
expected failures.



-- 
Steve


More information about the Python-ideas mailing list