LBYL vs EAFP

Discussion in 'Python' started by Steven D'Aprano, Feb 4, 2013.

  1. The eternal conflict between "Look Before You Leap" and "Easier to Ask for
    Forgiveness than Permission" (LBYL vs EAFP) continues...

    I want to check that a value is a number. Let's say I don't care what sort
    of number -- float, int, complex, Fraction, Decimal, something else -- just
    that it is a number. Should I:

    Look Before I Leap:

    from numbers import Number
    if isinstance(x, Number):
    ...
    else:
    raise TypeError


    or Ask Forgiveness:

    x + 0
    ...


    where in both cases the ellipsis ... is the code I actually care about.

    The second version is more compact and easier to write, but is it easier to
    read?

    Does your answer change if I then go on to check the range of the number,
    e.g. to test that x is positive?



    A third option is not to check x at all, and hope that it will blow up at
    some arbitrary place in the middle of my code rather than silently do the
    wrong thing. I don't like this idea because, even if it fails, it is better
    to fail earlier than later.

    Comments, thoughts and opinions please.



    --
    Steven
     
    Steven D'Aprano, Feb 4, 2013
    #1
    1. Advertising

  2. On Tue, Feb 5, 2013 at 10:16 AM, Steven D'Aprano
    <> wrote:
    > A third option is not to check x at all, and hope that it will blow up at
    > some arbitrary place in the middle of my code rather than silently do the
    > wrong thing. I don't like this idea because, even if it fails, it is better
    > to fail earlier than later.
    >
    > Comments, thoughts and opinions please.


    It depends on what could cause the failure. If only a programming
    error could cause x to not be a number, I'd go with your third option
    - let it blow up anywhere, and follow the trace. That option requires
    zero forethought, which translates directly into programmer
    efficiency. But if x came from a user, then I'd be checking inputs at
    a much earlier point.

    Those are the two obvious cases though, and I'm assuming your
    situation is neither of them. Writing library code is half way in
    between. With the specific examples given, I wouldn't like to use "x +
    0" as a check; it seems dodgy. Firstly because it doesn't look like a
    data type check (though a comment can help with that), and secondly
    because something might very well support having a number added to it
    while definitely not itself being a number - eg something along the
    lines of a database cursor. So I would recommend LBYL for this
    particular case.

    ChrisA
     
    Chris Angelico, Feb 4, 2013
    #2
    1. Advertising

  3. Steven D'Aprano

    Ian Kelly Guest

    On Feb 4, 2013 4:24 PM, "Steven D&apos;Aprano" <
    > wrote:
    >
    > The eternal conflict between "Look Before You Leap" and "Easier to Ask for
    > Forgiveness than Permission" (LBYL vs EAFP) continues...
    >
    > I want to check that a value is a number. Let's say I don't care what sort
    > of number -- float, int, complex, Fraction, Decimal, something else --

    just
    > that it is a number. Should I:
    >
    > Look Before I Leap:
    >
    > from numbers import Number
    > if isinstance(x, Number):
    > ...
    > else:
    > raise TypeError
    >
    >
    > or Ask Forgiveness:
    >
    > x + 0
    > ...
    >
    >
    > where in both cases the ellipsis ... is the code I actually care about.


    It seems to me that both of these are LBYL. That the second test checks by
    trying an operation and potentially raising an exception is immaterial.
    You're still performing a test prior to attempting the actual operation.

    > A third option is not to check x at all, and hope that it will blow up at
    > some arbitrary place in the middle of my code rather than silently do the
    > wrong thing. I don't like this idea because, even if it fails, it is

    better
    > to fail earlier than later.


    This is what I would consider EAFP. Presumably if the operation requires a
    number, then it will at some point perform some kind of numerical
    manipulation that will raise a TypeError if one is not passed. If the
    operation succeeds, then the object supported all the operations you asked
    of it, so in what sense would the program be doing the wrong thing?
     
    Ian Kelly, Feb 4, 2013
    #3
  4. Steven D'Aprano

    Dave Angel Guest

    On 02/04/2013 06:38 PM, Chris Angelico wrote:
    > On Tue, Feb 5, 2013 at 10:16 AM, Steven D'Aprano
    > <> wrote:
    >> A third option is not to check x at all, and hope that it will blow up at
    >> some arbitrary place in the middle of my code rather than silently do the
    >> wrong thing. I don't like this idea because, even if it fails, it is better
    >> to fail earlier than later.
    >>
    >> Comments, thoughts and opinions please.

    >
    > It depends on what could cause the failure. If only a programming
    > error could cause x to not be a number, I'd go with your third option
    > - let it blow up anywhere, and follow the trace. That option requires
    > zero forethought, which translates directly into programmer
    > efficiency. But if x came from a user, then I'd be checking inputs at
    > a much earlier point.
    >
    > Those are the two obvious cases though, and I'm assuming your
    > situation is neither of them. Writing library code is half way in
    > between. With the specific examples given, I wouldn't like to use "x +
    > 0" as a check; it seems dodgy. Firstly because it doesn't look like a
    > data type check (though a comment can help with that), and secondly
    > because something might very well support having a number added to it
    > while definitely not itself being a number - eg something along the
    > lines of a database cursor. So I would recommend LBYL for this
    > particular case.
    >
    > ChrisA
    >


    I agree with Chris. It would seem that the main purpose of the abc
    numbers.Number is for this. See the page:

    http://docs.python.org/2/library/numbers.html
    "If you just want to check if an argument x is a number, without
    caring what kind, use isinstance(x, Number)."

    But the real question is "what is a number?" . You used lowercase in
    your description, so you weren't defining it as the particular classes
    that were already associated with Number. I expect you're using Python
    3, which you should have specified. If so, I'd tend to replace the x+0
    with x > 0 which would be an error for any class other than int which
    didn't define the corresponding dunder operators.


    Coincidentally, that's the next question you asked. So if you're only
    interested in values that are positive, that would seem to be clearer.

    Unfortunately, that test would raise a TypeError for complex values.



    --
    DaveA
     
    Dave Angel, Feb 4, 2013
    #4
  5. Steven D'Aprano

    Ethan Furman Guest

    On 02/04/2013 03:16 PM, Steven D'Aprano wrote:
    > The eternal conflict between "Look Before You Leap" and "Easier to Ask for
    > Forgiveness than Permission" (LBYL vs EAFP) continues...
    >
    > I want to check that a value is a number. Let's say I don't care what sort
    > of number -- float, int, complex, Fraction, Decimal, something else -- just
    > that it is a number. Should I:
    >
    > Look Before I Leap:
    >
    > from numbers import Number
    > if isinstance(x, Number):
    > ...
    > else:
    > raise TypeError
    >
    >
    > or Ask Forgiveness:
    >
    > x + 0
    > ...


    As Ian mentioned, both cases are LYBL, unless of course your addition
    was just an example of some mathematical code you have further down.

    Personally, I go with EAFP unless I'm trying to present friendlier error
    messages.

    ~Ethan~
     
    Ethan Furman, Feb 5, 2013
    #5
  6. On Tue, Feb 5, 2013 at 10:16 AM, Steven D'Aprano
    <> wrote:
    > from numbers import Number
    > if isinstance(x, Number):
    > ...
    > else:
    > raise TypeError
    >
    >
    > or Ask Forgiveness:
    >
    > x + 0
    > ...
    >
    >
    > where in both cases the ellipsis ... is the code I actually care about.


    Caveat to my previous post: I would NOT indent the function body for
    the sake of this check. I'd negate it:

    if not isinstance(x, Number): raise TypeError

    (hopefully with further information in the TypeError). I don't like
    the code style that puts conditions, then more code, then error
    handling - I prefer to fail-and-bail.

    ChrisA
     
    Chris Angelico, Feb 5, 2013
    #6
  7. On 4 February 2013 23:16, Steven D'Aprano
    <> wrote:
    >
    > I want to check that a value is a number. Let's say I don't care what sort
    > of number -- float, int, complex, Fraction, Decimal, something else -- just
    > that it is a number. Should I:
    >
    > Look Before I Leap:
    >
    > from numbers import Number
    > if isinstance(x, Number):
    > ...
    > else:
    > raise TypeError
    >
    >
    > or Ask Forgiveness:
    >
    > x + 0
    > ...


    One example that passes in this case but would fail in many others is
    a numpy array:

    >>> from numpy import array
    >>> a = array([1, 2, 3])
    >>> a + 0

    array([1, 2, 3])
    >>> range(a)

    Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    TypeError: only length-1 arrays can be converted to Python scalars
    >>> bool(a)

    Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    ValueError: The truth value of an array with more than one element is
    ambiguous. Use a.any() or a.all()

    >
    >
    > where in both cases the ellipsis ... is the code I actually care about.
    >
    > The second version is more compact and easier to write, but is it easier to
    > read?
    >
    > Does your answer change if I then go on to check the range of the number,
    > e.g. to test that x is positive?
    >
    > A third option is not to check x at all, and hope that it will blow up at
    > some arbitrary place in the middle of my code rather than silently do the
    > wrong thing. I don't like this idea because, even if it fails, it is better
    > to fail earlier than later.


    Why would a non-number appear in this code? How serious a problem
    would it be if it did happen?

    The other question is whether there is likely to be a use-case for
    allowing non-standard numeric types (apart from dogmatic support of
    duck-typing).

    I'm imagining a scenario where you wrote a library and a user (another
    programmer) passed in the wrong thing (even though you clearly
    documented that you wanted a number).

    If you do check then they get this:

    # myuserscript.py
    import stevesmod
    stevesmod.calculate_with_numbers(number=file)

    $ python myuserscript.py
    Traceback (most recent call last):
    File "myuserscript.py", line 5, in <module>
    stevesmod.calculate_with_numbers(number=file)
    File ".local/pymodules/stevesmod.py", line 5, in calculate_with_numbers
    x + 0
    TypeError: unsupported operand type(s) for +: 'type' and 'int'

    If you don't check then they get:

    $ python myuserscript.py
    Traceback (most recent call last):
    File "myuserscript.py", line 5, in <module>
    stevesmod.calculate_with_numbers(number=file)
    File "/home/user/.local/pymodules/stevesmod.py", line 7, in
    calculate_with_numbers
    compute_stuff(number, other_parameters)
    File "/home/user/.local/pymodules/stevesmod.py", line 26, in compute_stuff
    normalise(nx, ny)
    File "/home/user/.local/pymodules/stevesmod.py", line 35, in normalise
    nx += 1
    TypeError: unsupported operand type(s) for +: 'type' and 'int'


    >From the users perspective the traceback gets more complicated. They

    still see the exact point at which there code calls into yours though.
    So if they know how to use a debugger and reread your docs then it's
    fine. (If you were doing something callback-based or storing the
    values for later use it would get more complicated).

    On the other hand if they just end up getting nonsensical return
    values then they should be able to trace that back.


    Oscar
     
    Oscar Benjamin, Feb 5, 2013
    #7
  8. On Tue, 05 Feb 2013 10:38:41 +1100, Chris Angelico wrote:

    > On Tue, Feb 5, 2013 at 10:16 AM, Steven D'Aprano
    > <> wrote:
    >> A third option is not to check x at all, and hope that it will blow up
    >> at some arbitrary place in the middle of my code rather than silently
    >> do the wrong thing. I don't like this idea because, even if it fails,
    >> it is better to fail earlier than later.

    [...]

    > With
    > the specific examples given, I wouldn't like to use "x + 0" as a check;
    > it seems dodgy. Firstly because it doesn't look like a data type check


    Strange. To me, it looks like "oh, you're testing whether x supports
    numeric addition". I suppose the hidden assumption I'm making is that if
    x supports addition, it is a kind of number. Perhaps that's an unsafe
    assumption.


    > (though a comment can help with that), and secondly because something
    > might very well support having a number added to it while definitely not
    > itself being a number - eg something along the lines of a database
    > cursor.


    But surely duck-typing tells us that if database cursors support the same
    sort of operations that numbers support, we should therefore treat them
    as a kind of number?

    My library will not try to prevent the caller taking the average of (say)
    a dozen eggs and 10 miles. I'm not sure that it should try to prevent the
    caller from trying to take the average between two database cursors.



    --
    Steven
     
    Steven D'Aprano, Feb 5, 2013
    #8
  9. On Mon, 04 Feb 2013 16:46:11 -0700, Ian Kelly wrote:

    > Presumably if the operation requires
    > a number, then it will at some point perform some kind of numerical
    > manipulation that will raise a TypeError if one is not passed. If the
    > operation succeeds, then the object supported all the operations you
    > asked of it, so in what sense would the program be doing the wrong
    > thing?


    It might not support *all* the operations. Consider a toy function like
    this:

    def toy(x): # Don't try this at home!
    return x*10000000000 + 1

    If you pass a non-empty list, Python will:

    - try to allocate a chunk of memory of at least 4 GB (estimated), which
    may cause quite a bit of thrashing;

    - if somehow this succeeds, then it will create a list and populate it
    with rather a lot of duplicated references;

    - at which point it will then try to add a list to an int, and raise an
    exception;

    - and then deallocate a huge list, causing more thrashing.


    So there are consequences to allowing exceptions to occur in arbitrary
    places.

    There's also the principle that it is best to raise an exception as early
    as possible. It's easier to track down errors at the point they are
    introduced than long afterwards.

    And finally, even worse than exceptions are silent failures of semantics:
    code that does the wrong thing rather than fail loudly and safely. Just
    because the code doesn't fail, doesn't mean it has worked, and an
    exception is much better than a silent failure. You seem to be making the
    classic mistake of thinking that exceptions are something to avoid:

    "I find it amusing when novice programmers believe their main job is
    preventing programs from crashing. [...] More experienced programmers
    realize that correct code is great, code that crashes could use
    improvement, but incorrect code that doesn’t crash is a horrible
    nightmare." -- Chris Smith

    http://cdsmith.wordpress.com/2011/01/09/an-old-article-i-wrote/


    Duck-typing is not a panacea, and it too has failure modes. The usual
    example is, suppose you have a graphics application that expects an
    object with a "draw" method:

    pencil.draw()
    paintbrush.draw()
    crayon.draw()
    six_shooter.draw() # bang, you've just shot yourself in the foot


    So I lean very strongly to some sort of explicit check ahead of time. I'm
    just not sure *what sort* of explicit check.


    --
    Steven
     
    Steven D'Aprano, Feb 5, 2013
    #9
  10. On Tue, Feb 5, 2013 at 2:52 PM, Steven D'Aprano
    <> wrote:
    > On Tue, 05 Feb 2013 10:38:41 +1100, Chris Angelico wrote:
    >
    >> On Tue, Feb 5, 2013 at 10:16 AM, Steven D'Aprano
    >> <> wrote:
    >>> A third option is not to check x at all, and hope that it will blow up
    >>> at some arbitrary place in the middle of my code rather than silently
    >>> do the wrong thing. I don't like this idea because, even if it fails,
    >>> it is better to fail earlier than later.

    > [...]
    >
    >> With
    >> the specific examples given, I wouldn't like to use "x + 0" as a check;
    >> it seems dodgy. Firstly because it doesn't look like a data type check

    >
    > Strange. To me, it looks like "oh, you're testing whether x supports
    > numeric addition". I suppose the hidden assumption I'm making is that if
    > x supports addition, it is a kind of number. Perhaps that's an unsafe
    > assumption.


    If your code bombs and the exception traceback shows a line saying "x
    + 0", I'm going to wonder if it's a massive bug... or, worse, if
    you're expecting x.__add__() to have stupid side effects ("yeah,
    adding 0 to x turns x into a float, but subtracting 0 from it forces
    it to be an int").

    >> (though a comment can help with that), and secondly because something
    >> might very well support having a number added to it while definitely not
    >> itself being a number - eg something along the lines of a database
    >> cursor.

    >
    > But surely duck-typing tells us that if database cursors support the same
    > sort of operations that numbers support, we should therefore treat them
    > as a kind of number?


    Depends what operations you mean. It's not uncommon for a
    cursor/iterator object to support "x+=1" to move to the next
    row/element/whatever; if your result set is maintained elsewhere, you
    could cheaply support "x + 4" to create a new iterator that's four
    rows further along. But in all other respects, it's not a number. You
    can't multiply an iterator by 2, for instance.

    > My library will not try to prevent the caller taking the average of (say)
    > a dozen eggs and 10 miles. I'm not sure that it should try to prevent the
    > caller from trying to take the average between two database cursors.


    Well, sure... if your code is so simple and trivial, and if the
    cursors support "x - y" to get the number of rows between them. But in
    that case, go for true EAFP and eschew error checking altogether.

    ChrisA
     
    Chris Angelico, Feb 5, 2013
    #10
  11. On Tue, Feb 5, 2013 at 3:52 PM, Steven D'Aprano
    <> wrote:
    > There's also the principle that it is best to raise an exception as early
    > as possible. It's easier to track down errors at the point they are
    > introduced than long afterwards.


    Yes, definitely, especially (as was mentioned) if you're working with
    callbacks. But I'd use isinstance then.

    ChrisA
     
    Chris Angelico, Feb 5, 2013
    #11
  12. Steven D'Aprano

    Ian Kelly Guest

    On Mon, Feb 4, 2013 at 9:52 PM, Steven D'Aprano
    <> wrote:
    > You seem to be making the
    > classic mistake of thinking that exceptions are something to avoid:


    Far from it. You've extrapolated a lot more than what I actually
    said, and I completely agree with everything you wrote. I was
    explaining EAFP as I see it, not advocating it for all circumstances.

    Although since you bring it up, I find that the LBYL crowd tends to be
    more prone to exception avoidance, e.g. returning None on a failure
    rather than raising an exception, whereas the EAFP crowd seems more
    likely to just let the original exception propagate up.
     
    Ian Kelly, Feb 5, 2013
    #12
  13. On Tue, 05 Feb 2013 16:20:19 +1100, Chris Angelico wrote:

    > On Tue, Feb 5, 2013 at 3:52 PM, Steven D'Aprano
    > <> wrote:
    >> There's also the principle that it is best to raise an exception as
    >> early as possible. It's easier to track down errors at the point they
    >> are introduced than long afterwards.

    >
    > Yes, definitely, especially (as was mentioned) if you're working with
    > callbacks. But I'd use isinstance then.



    I'm leaning towards an isinstance check

    I've been using Python since Python 1.5. Even though 1.5 had an
    "isinstance" function, I learned Python from books and code written for
    Python 1.4 which did not have that function, so the usual way to do type-
    checking was:


    if type(obj) is type(1):
    # it's an int


    and strongly discouraged. So my instincts are still very strongly primed
    to *not* do type-checking if I can avoid it. But in this case, I think I
    agree with those suggesting the isinstance check is the right approach.
    The main downside to this is that objects which delegate to a number will
    not work unless they are explicitly registered with the Number ABC.


    Thanks to everyone who responded.



    --
    Steven
     
    Steven D'Aprano, Feb 5, 2013
    #13
  14. Steven D'Aprano

    Terry Reedy Guest

    On 2/4/2013 6:16 PM, Steven D'Aprano wrote:
    > The eternal conflict between "Look Before You Leap" and "Easier to Ask for
    > Forgiveness than Permission" (LBYL vs EAFP) continues...


    A somewhat different answer is that it depends on what you want the
    function to do, as documented and *tested*. And that partly depends on
    whether it is educational code for humans, production app code, or
    library code.

    The test driven approach would be to write tests and then do what is
    needed to get them to pass. Doctests, unittests, and my private function
    test functions allow testing for raising a particular exception.
    AssertRaises() is used, perhaps increasingly, in stdlib tests. The
    absence of any tests for the response to 'bad' input suggests that the
    responses are 'undefined'.

    That said, I admit that Python's extensible class system makes bad-input
    testing harder. I also think that anyone who uses non-builtin classes
    outside of their intended use area has to take responsibility.

    For instance:

    def f(n, a):
    if n < 0: raise ValueError('n cannot be negative')
    b = 0
    while n:
    b = process(a, b)
    n -= 1

    looks like a safe LBYL function. But suppose n is an instance of a class
    that perversely implements subtraction as addition (or as doing
    nothing). Algorithm termination is based on the presumption that
    'decrementing' a 'positive value' moves it 'toward 0' and can only be
    done a finite number of times. Verifying that an input is a member of
    that abstract class is not trivial ;-).

    > A third option is not to check x at all, and hope that it will blow
    > up at some arbitrary place in the middle of my code rather than
    > silently do the wrong thing.


    A silent infinite loop is bad. Infinite recursion stopped with the
    recursion limit check is less bad.

    If tests pass with no check, then nothing need be done until one moves
    from correctness to resource use.

    --
    Terry Jan Reedy
     
    Terry Reedy, Feb 5, 2013
    #14
  15. Steven D'Aprano

    Pete Forman Guest

    Steven D'Aprano <> writes:
    >> I want to check that a value is a number. [...]

    > I'm leaning towards an isinstance check


    Well that is the answer to your question, whether the value *is* a
    number. EAFP can answer the question whether the value *behaves* like a
    number, where the criterion depends on what your code is aiming to do
    with the value.

    BTW what if the value is Not-a-Number? ;-)

    --
    Pete Forman
     
    Pete Forman, Feb 5, 2013
    #15
  16. Pete Forman wrote:

    > Steven D'Aprano <> writes:
    >>> I want to check that a value is a number. [...]

    >> I'm leaning towards an isinstance check

    [...]
    > BTW what if the value is Not-a-Number? ;-)


    Nothing different, and hopefully exactly what the caller expects. As far as
    Python is concerned, NANs are Numbers.

    py> NAN = float('nan')
    py> from numbers import Number
    py> isinstance(NAN, Number)
    True


    If it's a float NAN, Python doesn't give you much control over what happens
    next, but generally any arithmetic operation on a NAN will return a NAN
    rather than raise.

    If it's a Decimal NAN, the same applies:

    py> NAN = decimal.Decimal('nan')
    py> NAN + 0
    Decimal('NaN')


    If it's a Decimal SNAN (signalling NAN), then arithmetic operations signal
    InvalidOperation, which by default will raise an exception:

    py> SNAN = decimal.Decimal('snan')
    py> SNAN + 0
    Traceback (most recent call last):
    ...
    decimal.InvalidOperation: sNaN




    --
    Steven
     
    Steven D'Aprano, Feb 5, 2013
    #16
  17. On Tue, Feb 5, 2013 at 11:04 PM, Steven D'Aprano
    <> wrote:
    > py> isinstance(NAN, Number)
    > True


    Does that line of code count as nerd humour?

    ChrisA
     
    Chris Angelico, Feb 5, 2013
    #17
    1. Advertising

Want to reply to this thread or ask your own question?

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. Arnaud Delobelle

    EAFP gone wrong

    Arnaud Delobelle, Feb 9, 2010, in forum: Python
    Replies:
    4
    Views:
    276
    Arnaud Delobelle
    Feb 10, 2010
Loading...

Share This Page