16

This is a rather useless assertion error; it does not tell the values of the expression involved (assume constants used are actually variable names):

$ python -c "assert 6-(3*2)"
[...]
AssertionError

Is there a better assert implementation in Python that is more fancy? It must not introduce additional overhead over execution (except when assert fails) .. and must turn off if -O flag is used.

Edit: I know about assert's second argument as a string. I don't want to write one .. as that is encoded in the expression that is being asserted. DRY (Don't Repeat Yourself).

NullUserException
  • 83,810
  • 28
  • 209
  • 234
Sridhar Ratnakumar
  • 81,433
  • 63
  • 146
  • 187

7 Answers7

11

Install your of function as sys.excepthook -- see the docs. Your function, if the second argument is AssertionError, can introspect to your heart's contents; in particular, through the third argument, the traceback, it can get the frame and exact spot in which the assert failed, getting the failing exception through the source or bytecode, the value of all relevant variables, etc. Module inspect helps.

Doing it in full generality is quite a piece of work, but depending on what constraints you're willing to accept in how you write your asserts it can be lightened substantially (e.g. restricting them to only local or global variables makes introspection easier than if nonlocal variables of a closure could be involved, and so forth).

Alex Martelli
  • 854,459
  • 170
  • 1,222
  • 1,395
  • 2
    Good. Now is there a Python library for this .. or do I have to write my own? :-) (I probably won't .. as this is a low-prio task for me) – Sridhar Ratnakumar Aug 21 '09 at 19:40
  • Unfortunately I don't know of existing Python libraries doing all of this, except ones oriented to testing (which might have to be adapted for the purpose of using them on production code). – Alex Martelli Aug 22 '09 at 18:36
7

You can attach a message to an assert:

assert 6-(3*2), "always fails"

The message can also be built dynamically:

assert x != 0, "x is not equal to zero (%d)" % x

See The assert statement in the Python documentation for more information.

Greg Hewgill
  • 951,095
  • 183
  • 1,149
  • 1,285
7

As @Mark Rushakoff said nose can evaluate failed asserts. It works on the standard assert too.

# test_error_reporting.py
def test():
    a,b,c = 6, 2, 3
    assert a - b*c

nosetests' help:

$ nosetests --help|grep -B2 assert
  -d, --detailed-errors, --failure-detail
                        Add detail to error output by attempting to evaluate
                        failed asserts [NOSE_DETAILED_ERRORS]

Example:

$ nosetests -d
F
======================================================================
FAIL: test_error_reporting.test
----------------------------------------------------------------------
Traceback (most recent call last):
  File "..snip../site-packages/nose/case.py", line 183, in runTest
    self.test(*self.arg)
  File "..snip../test_error_reporting.py", line 3, in test
    assert a - b*c
AssertionError:
    6,2,3 = 6, 2, 3
>>  assert 6 - 2*3


----------------------------------------------------------------------
Ran 1 test in 0.089s

FAILED (failures=1)
Community
  • 1
  • 1
jfs
  • 399,953
  • 195
  • 994
  • 1,670
  • The question is regarding the use of assert in application code (which is directly invoked by the user, for eg., ./foo.py .. or clicking on 'foo.pyw' on Windows Explorer), and not test code .. for which I am actually happy with py.test's assert output. – Sridhar Ratnakumar Aug 20 '09 at 21:54
  • 1
    @srid: In this case write: `__debug__ and your_fancy_assert(expression)` -- no overhead on '-O'. – jfs Aug 20 '09 at 22:04
  • That's sounds interesting; too bad Python doesn't have a `macro` feature. – Sridhar Ratnakumar Aug 20 '09 at 23:23
  • 1
    @SridharRatnakumar: it has (now) [`macropy`](https://github.com/lihaoyi/macropy#smart-asserts) – jfs Oct 28 '13 at 23:15
  • For the introspection to work, you also need to leave out the optional `msg` parameter. – congusbongus Aug 10 '15 at 06:31
4

The nose testing suite applies introspection to asserts.

However, AFAICT, you have to call their asserts to get the introspection:

import nose
def test1():
    nose.tools.assert_equal(6, 5+2)

results in

C:\temp\py>C:\Python26\Scripts\nosetests.exe -d test.py
F
======================================================================
FAIL: test.test1
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Python26\lib\site-packages\nose-0.11.1-py2.6.egg\nose\case.py", line
183, in runTest
    self.test(*self.arg)
  File "C:\temp\py\test.py", line 3, in test1
    nose.tools.assert_equal(6, 5+2)
AssertionError: 6 != 7
>>  raise self.failureException, \
          (None or '%r != %r' % (6, 7))

Notice the AssertionError there. When my line was just assert 6 == 5+2, I would get:

C:\temp\py>C:\Python26\Scripts\nosetests.exe -d test.py
F
======================================================================
FAIL: test.test1
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Python26\lib\site-packages\nose-0.11.1-py2.6.egg\nose\case.py", line
183, in runTest
    self.test(*self.arg)
  File "C:\temp\py\test.py", line 2, in test1
    assert 6 == 5 + 2
AssertionError:
>>  assert 6 == 5 + 2

Also, I'm not sure offhand if their asserts are skipped with -O, but that would be a very quick check.

Mark Rushakoff
  • 249,864
  • 45
  • 407
  • 398
  • Good enough for test cases, but for production code .. there is function call overhead (even with -O option) – Sridhar Ratnakumar Aug 20 '09 at 21:21
  • 4
    Ordinary asserts work too. See http://stackoverflow.com/questions/1308607/python-assert-improved-introspection-of-failure/1309039#1309039 – jfs Aug 20 '09 at 21:50
3

I coded a replacement for sys.excepthook (which is called for any unhandled exception) which is a bit more fancy than the standard one. It will analyze the line where the exception occured and print all variables which are referred to in this line (it does not print all local variables because that might be too much noise - also, maybe the important var is global or so).

I called it py_better_exchook (perfect name) and it's here.

Example file:

a = 6

def test():
    unrelated_var = 43
    b,c = 2, 3
    assert a - b*c

import better_exchook
better_exchook.install()

test()

Output:

$ python test_error_reporting.py 
EXCEPTION
Traceback (most recent call last):
  File "test_error_reporting.py", line 12, in <module>
    line: test()
    locals:
      test = <local> <function test at 0x7fd91b1a05f0>
  File "test_error_reporting.py", line 7, in test
    line: assert a - b*c
    locals:
      a = <global> 6
      b = <local> 2
      c = <local> 3
AssertionError

There are a few other alternatives:

Albert
  • 65,406
  • 61
  • 242
  • 386
  • Can this be used in the same way as `import traceback; traceback.print_stack()`, ie, printing then continuing execution? (I tried looking at the code but it was beyond me.) Huge thanks, very glad to discover this, been wanting to get better exception reports for years! – Chris Sep 21 '20 at 08:57
  • 1
    @Chris Yes sure. You just catch the exception in `try: ... except Exception: ...`, then you print the exception with all info by `sys.excepthook(*sys.exc_info())`, and then it continues. – Albert Sep 21 '20 at 09:59
  • Thank you! Is artificially raising an exception, eg, `try:raise Exception();except Exception:sys.excepthook(*sys.exc_info())` the best way even if there's no exception and I'm just looking for a dump of state at a point in working code? – Chris Sep 21 '20 at 10:38
  • On testing this, it just seems to report the `try` block scope at the lowest level, nothing higher and no variables outside the lowest `try` block (so just `Exception` in this case). – Chris Sep 21 '20 at 11:12
  • 1
    If you don't have an exception, you don't need to raise a dummy one. You could directly call `better_exchook.print_tb(None)`, which will print the current traceback. – Albert Sep 21 '20 at 11:28
  • Wow! I'm working on a tangled performance issue and this will be invaluable, thanks! – Chris Sep 21 '20 at 11:39
  • 1
    Btw, printing the stacktrace / exceptions is usually helpful on unexpected exceptions. For debugging, if possible, it is usually better to use a real debugger in an IDE. E.g. I can recommend PyCharm (the free community version is just fine). You just put a breakpoint where ever you want, and then you get all information you want. – Albert Sep 21 '20 at 12:26
  • Thanks very much, I do use PyCharm but this only runs inside a couple Docker containers (and I find the PyCharm Docker integration clunky) and it is a bit of a forensic exercise figuring out where the flow is going in a long batch processing sequence, so this is perfect. – Chris Sep 21 '20 at 17:18
  • 10/10 for mentioning alternatives to your own library! – jtlz2 Mar 13 '22 at 18:08
1

It sounds like what you really want to do is to set up a debugger breakpoint just before the assert and inspect from your favorite debugger as much as you like.

ilya n.
  • 18,398
  • 15
  • 71
  • 89
0

Add a message to your assertion, which will be displayed if the assertion fails:

$ python -c "assert 6-(3*2), '6-(3*2)'"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
AssertionError: 6-(3*2)

The only way I can think of to provide this automatically would be to contain the assertion in a procedure call, and then inspect the stack to get the source code for that line. The additional call would, unfortunately, introduce overhead into the test and would not be disabled with -O.

John Millikin
  • 197,344
  • 39
  • 212
  • 226