7

There is this code:

class MyException(Exception):
  pass

def gen():
  for i in range(3):
    try:
      yield i
    except MyException:
      print("MyException!")


a = gen()
next(a) 
a.throw(MyException)

Running this code:

$ python3.3 main.py
MyException!
$ python3.3 main.py
MyException!
Exception TypeError: TypeError('catching classes that do not inherit from BaseException is not allowed',) in <generator object gen at 0xb712efa4> ignored
$ python3.3 main.py
MyException!
$ python3.3 main.py
MyException!
$ python3.3 main.py
MyException!
Exception TypeError: TypeError('catching classes that do not inherit from BaseException is not allowed',) in <generator object gen at 0xb714afa4> ignored

The thing which I don't understand is why sometimes there is printed this Exception TypeError warning. Is there something wrong with custom exception?

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
scdmb
  • 15,091
  • 21
  • 85
  • 128

3 Answers3

12

You are seeing a __del__ hook misbehaving somewhere.

The TypeError is being thrown while shutting down, as the Python interpreter is exiting everything is deleted and any exceptions thrown in a __del__ deconstructor hook are being ignored (but are printed).

On exit, Python clears everything in the namespace by rebinding everything to None, but the order in which this happens is not set. The still running generator is closed (a.close() is called) when deleted, which triggers a GeneratorExit exception in the generator, which Python tests against your except MyException: line. If, however, MyException has already been cleared up and Python sees except None: the TypeError is thrown and you see that message printed.

You can trigger the error without exiting Python by adding:

MyException = None
del a

If you use list(a) and consume the rest of the generator, or explicitly close the generator with a.close() before Python exits and deletes MyException, the error message goes away.

Another work-around would be to handle GeneratorExit first:

def gen():
  for i in range(3):
    try:
      yield i
    except GeneratorExit:
      return
    except MyException:
      print("MyException!")

and Python will not evaluate the next except handler.

The error cannot be reproduced with Python 3.2 or earlier, so it looks like hash randomization (introduced in Python 3.3) randomizes the order objects are cleared; this certainly explains why you see the error only on some of your runs, but not on earlier Python runs where the hash order is fixed.

Note that the interaction of .__del__() hooks and other global objects in Python is documented with a big red warning in the .__del__() documentation:

Warning: Due to the precarious circumstances under which __del__() methods are invoked, exceptions that occur during their execution are ignored, and a warning is printed to sys.stderr instead. Also, when __del__() is invoked in response to a module being deleted (e.g., when execution of the program is done), other globals referenced by the __del__() method may already have been deleted or in the process of being torn down (e.g. the import machinery shutting down). For this reason, __del__() methods should do the absolute minimum needed to maintain external invariants. Starting with version 1.5, Python guarantees that globals whose name begins with a single underscore are deleted from their module before other globals are deleted; if no other references to such globals exist, this may help in assuring that imported modules are still available at the time when the __del__() method is called.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • in python 2.7 the warning is never shown and code posted is the only code in the file. – scdmb Aug 10 '13 at 16:10
  • Python 3.2 also doesn't print that. – Pavel Anossov Aug 10 '13 at 16:12
  • With `del`, the much more logical NameError is raised. – Pavel Anossov Aug 10 '13 at 16:40
  • @PavelAnossov: Because then the exception is not rebound to `None`. I've updated my answer to better reflect that. :-) – Martijn Pieters Aug 10 '13 at 16:40
  • Seems like a bad idea to execute python code after rebinding a bunch of names to `None`. – Pavel Anossov Aug 10 '13 at 16:50
  • This is not limited to generators. This has the same problems: http://pastebin.com/FLDfePS6 – Pavel Anossov Aug 10 '13 at 16:55
  • @PavelAnossov: This is well-documented, see http://docs.python.org/3/reference/datamodel.html#object.__del__ – Martijn Pieters Aug 10 '13 at 16:56
  • @PavelAnossov: I knew about the behaviour of `__del__` and exceptions being printed to `stderr` already, which is why I realised that the error was being shown on shutdown and that it must be related to object deletion. – Martijn Pieters Aug 10 '13 at 17:00
  • @MartijnPieters: right, so it's not because of generator changes in 3.3, it's just something about the deletion order changed. – Pavel Anossov Aug 10 '13 at 17:01
  • @PavelAnossov: Perhaps; the deconstructor has been closing generators explicitly since [PEP 342 was implemented](http://hg.python.org/cpython/diff/b2fe5f8fcfdd/Objects/genobject.c), so it appears that that is not it. I now suspect it is the [hash randomization](http://bugs.python.org/issue13703) that causes the order of deletion to change randomly. – Martijn Pieters Aug 10 '13 at 17:07
  • I feel less safe now :) The general advice "do not use `__del__`" is now extended with "be aware that some of your code may get called from some built-in `__del__` and everything about `__del__` applies to it". Another advice is "do not leave unclosed generators around". – Pavel Anossov Aug 10 '13 at 17:15
  • @MartijnPieters I know one shouldn't normally leave comments like this but...figuring this one out is *extremely* impressive! thanks! – Mike Vella Sep 02 '13 at 20:25
2

I was having this same error in Python 3.3 on Windows, with the difference that I was defining the exception in its own file. These were my code files:

$ cat FooError.py 
class FooError(Exception):
    pass

$ cat application.py
import FooError
try:
    raise FooError('Foo not bar!')
Except FooError as e:
    print(e)

This was the exception that I was getting:

TypeError: catching classes that do not inherit from BaseException is not allowed.

Changing import FooError to from FooError import * resolved the issue. Here is the final code, for clarity's sake:

$ cat FooError.py 
class FooError(Exception):
    pass

$ cat application.py
from FooError import *
try:
    raise FooError('Foo not bar!')
Except FooError as e:
    print(e)
dotancohen
  • 30,064
  • 36
  • 138
  • 197
  • Would have worked if you just Except FooError.FooError since you can't catch the module :) – Guy L Aug 11 '15 at 21:32
  • @GuyL: Thanks! I don't have a Windows machine to test, but I suppose that `from FooError import FooError` would have worked as well. – dotancohen Aug 12 '15 at 05:15
1

I had the same problem - but I was missing the import to the exception class. So the interpreter didn't resolve the class on the except clause.

So just add the import and hopefully everything will work.

Guy L
  • 2,824
  • 2
  • 27
  • 37