58

Can I make assert throw an exception that I choose instead of AssertionError?

UPDATE:

I'll explain my motivation: Up to now, I've had assertion-style tests that raised my own exceptions; For example, when you created a Node object with certain arguments, it would check if the arguments were good for creating a node, and if not it would raise NodeError.

But I know that Python has a -o mode in which asserts are skipped, which I would like to have available because it would make my program faster. But I would still like to have my own exceptions. That's why I want to use assert with my own exceptions.

Ram Rachum
  • 84,019
  • 84
  • 236
  • 374
  • Ok, thanks for the extra info. Why do you want to use your own exceptions? – Ned Batchelder Oct 14 '09 at 21:43
  • When something's wrong with creating a Node, it's an issue with the Node's logic, and therefore it seems sensible to me that NodeError will be raised. – Ram Rachum Oct 14 '09 at 21:48
  • 1
    Python style is to use more of the built-in exceptions for cases like this. You shouldn't catch an AssertionError anyway, so you don't need to coordinate this with callers of your code. I say just use an AssertionError and go with the flow. – Ned Batchelder Oct 15 '09 at 02:01

8 Answers8

65

This will work. But it's kind of crazy.

try:
    assert False, "A Message"
except AssertionError, e:
    raise Exception( e.args )

Why not the following? This is less crazy.

if not someAssertion: raise Exception( "Some Message" )

It's only a little wordier than the assert statement, but doesn't violate our expectation that assert failures raise AssertionError.

Consider this.

def myAssert( condition, action ):
    if not condition: raise action

Then you can more-or-less replace your existing assertions with something like this.

myAssert( {{ the original condition }}, MyException( {{ the original message }} ) )

Once you've done this, you are now free to fuss around with enable or disabling or whatever it is you're trying to do.

Also, read up on the warnings module. This may be exactly what you're trying to do.

S.Lott
  • 384,516
  • 81
  • 508
  • 779
  • 1
    I want to be able to enjoy the `-o` option. Your crazy solution might be okay, if the try doesn't cause too much overhead. – Ram Rachum Oct 14 '09 at 21:25
  • 1
    To check for the -o optiont you can simply say `if __debug__ and not someAssertion:` – Ber Oct 14 '09 at 21:40
  • 1
    I think try does have overhead. Try it yourself http://stackoverflow.com/questions/1569049/making-pythons-assert-throw-an-exception-that-i-choose/1569618#1569618 – John La Rooy Oct 14 '09 at 23:54
  • 2
    `try` has at least a few opcodes of overhead - `SETUP_EXCEPT`, `JUMP_FORWARD`/`JUMP_ABSOLUTE` and `POP_BLOCK`. – Nick Bastin Jun 23 '10 at 18:25
  • @Nick Bastin: Yes, there's some overhead. try/except sometimes turns out faster than if. Haven't run any benchmarks because it's never been the bottleneck for me. – S.Lott Jun 23 '10 at 20:05
  • @S.Lott: Sure, but the whole goal here is to not even use `if` in production, and only test in development. – Nick Bastin Jun 23 '10 at 20:32
  • @Nick Bastin: I think it would be more clear to use a function definition and with two implementations. One that has an `if` statement and one that has `pass`. This would be less astonishing for folks who will have to maintain and adapt this. – S.Lott Jun 23 '10 at 20:37
  • +1 for myAssert(). Before I knew how to properly use assert(), I wrote my own "xassert" that threw AssertionError. Same idea but instead throw own assertion. – David Poole Oct 21 '11 at 13:27
28

How about this?


>>> def myraise(e): raise e
... 
>>> cond=False
>>> assert cond or myraise(RuntimeError)
Traceback (most recent call last):
  File "", line 1, in 
  File "", line 1, in myraise
RuntimeError

John La Rooy
  • 295,403
  • 53
  • 369
  • 502
15

Never use an assertion for logic! Only for optional testing checks. Remember, if Python is running with optimizations turned on, asserts aren't even compiled into the bytecode. If you're doing this, you obviously care about the exception being raised and if you care, then you're using asserts wrong in the first place.

ironfroggy
  • 7,991
  • 7
  • 33
  • 44
  • 2
    This way you at least put the "optimization" to work. Who ever uses optimization!? It seems pointless at the outset; using more assertions like this at least provides you with more optimization choice, so it's interesting. – u0b34a0f6ae Oct 15 '09 at 00:04
11

Python also skips if __debug__: blocks when run with -o option. The following code is more verbose, but does what you need without hacks:

def my_assert(condition, message=None):
    if not condition:
        raise MyAssertError(message)

if __debug__: my_assert(condition, message)

You can make it shorter by moving if __debug__: condition inside my_assert(), but then it will be called (without any action inside) when optimization is enabled.

Denis Otkidach
  • 32,032
  • 8
  • 79
  • 100
  • Thanks, I did not know __debug__ worked like that. I added it to my testcases. The answer currently with the most votes *does* have overhead however. – John La Rooy Oct 15 '09 at 22:06
6

You can let a context manager do the conversion for you, inside a with block (which may contain more than one assertion, or more code and function calls or what you want.

from __future__ import with_statement
import contextlib

@contextlib.contextmanager
def myassert(exctype):
    try:
        yield
    except AssertionError, exc:
        raise exctype(*exc.args)

with myassert(ValueError):
    assert 0, "Zero is bad for you"

See a previous version of this answer for substituting constructed exception objects directly (KeyError("bad key")), instead of reusing the assertions' argument(s).

u0b34a0f6ae
  • 48,117
  • 14
  • 92
  • 101
4

In Python 2.6.3 at least, this will also work:

class MyAssertionError (Exception):
    pass

AssertionError = MyAssertionError

assert False, "False"

Traceback (most recent call last):
  File "assert.py", line 8, in <module>
    assert False, "False"
__main__.MyAssertionError: False
Ber
  • 40,356
  • 16
  • 72
  • 88
  • Actually, to me this doesn't look too bad, though it only works at global level. You can't assign AssertionError inside a function and have the assert raise your exception. – Ned Batchelder Oct 14 '09 at 21:41
  • Assigning something to AssertionError is probably the only way. If you think you have a good idea and syntax why not write a PEP – John La Rooy Oct 14 '09 at 21:42
  • The only time I can recall using asserts lately is in unittests, which is a good place for them – John La Rooy Oct 14 '09 at 21:44
  • I think it can be very useful -- `AssertionError` is subclass of `Exception` and thus it can be accidentally suppressed in `except Exception`. – GingerPlusPlus Jul 15 '15 at 14:10
4

If you want to use asserts for this, this seems to work pretty well:

>>> def raise_(e): raise e
...
>>> x = -2
>>> assert x >= 0, raise_(ValueError('oops'))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in raise_
ValueError: oops

Note that in the assert, the stuff after the comma will only be evaluated if the condition is false, so the ValueError will only be created and raised when needed.

uryga
  • 402
  • 4
  • 14
  • Best answer, exactly what the question aimed for. IMO, the `raise_` is unnecessary, `assert x >= 0, raise ValueError('oops')` looks the same but without the function call overhead – YoavBZ Feb 08 '21 at 11:54
  • @YoavBZ your code gives me a syntax error on `3.8.0`. afaik the grammar requires the thing-after-the-comma to be an expression like `raise_(...)`, not a statement like `raise ...`, which is why I defined it :) – uryga Feb 08 '21 at 13:00
  • Gotcha, should have checked before writing ;) – YoavBZ Feb 08 '21 at 17:31
3

To see if try has any overhead I tried this experiment

here is myassert.py


def myassert(e):
    raise e

def f1(): #this is the control for the experiment cond=True

def f2(): cond=True try: assert cond, "Message" except AssertionError, e: raise Exception(e.args)

def f3(): cond=True assert cond or myassert(RuntimeError)

def f4(): cond=True if __debug__: raise(RuntimeError)


$ python -O -mtimeit -n100 -r1000 -s'import myassert' 'myassert.f1()'
100 loops, best of 1000: 0.42 usec per loop
$ python -O -mtimeit -n100 -r1000 -s'import myassert' 'myassert.f2()'
100 loops, best of 1000: 0.479 usec per loop
$ python -O -mtimeit -n100 -r1000 -s'import myassert' 'myassert.f3()'
100 loops, best of 1000: 0.42 usec per loop
$ python -O -mtimeit -n100 -r1000 -s'import myassert' 'myassert.f4()'
100 loops, best of 1000: 0.42 usec per loop
John La Rooy
  • 295,403
  • 53
  • 369
  • 502