Skip to main content

Raise exceptions with a function instead of a statement.

Project description

Raise exceptions with a function instead of a statement.

Provides a minimal, clean and portable interface for raising exceptions with all the advantages of functions over syntax.

Why

I want to be able to work with exceptions in a way that is:

  1. Intuitive to use and see in code.
  2. Generic and flexible, empowering reuse.
  3. Portable to all versions of Python I might want to use.

Python is a great language, and modern Python in particular takes a nice approach to exceptions.

In my code, I’ve often found myself writing interfaces that combine the intuitive nature of Python 3’s raise and with_traceback, the generic and flexible pattern of raising exceptions in other coroutines or threads of execution as exemplified by the throw method on Python generators, and the inherently portable and powerfully reusable and composable form of a basic function.

The interface provided by this module, the function signature taking an exception (either an instance or a type) and an optional traceback instance, is what I found myself arriving at that met all of these criteria. It has served me well in code that I’ve worked on, and I’m submitting it to the world in the hope that others will either find it useful and build upon it or point out flaws in my approach.

If you have a more specific “why” question, I recorded my reasons for a lot of the specific choices here in the Design Decisions section.

Versioning

This library’s version numbers follow the SemVer 2.0.0 specification.

The current version number is available in the variable __version__ as is normal for Python modules.

Installation

pip install raise

If you need or want to get it manually (for example if you’re using Brython or just cannot use pip), see the Advanced/Manual Installation section for suggestions/tips.

Usage

Import the raise_ function from the raise_ module:

from raise_ import raise_

Then you can raise things in a fairly intuitive manner:

  1. Raising an exception:

    raise_(Exception('foo'))
    

    You can of course also pass an exception type instead of an exception instance as the first argument to except_.

  2. Raising an exception with a traceback:

    raise_(Exception('foo'), traceback=my_traceback_object)
    

    traceback does not have to be passed as a keyword argument, but one advantage of doing so, is that this form (an exception as a positional argument and traceback as a keyword argument) fits the signature of Python’s generator throw method too.

Portability

Portable to all releases of both Python 3 and Python 2.

(The oldest tested is 2.5, but it will likely work on all Python 2 versions and probably on even earlier versions.)

Known not to work: MicroPython and Skulpt, both of which do not support the with_traceback exception method or the raise e, None, tb syntax.

Advanced/Manual Installation

There are two recommended ways of installing this manually, depending on your needs:

  1. If you’re installing it into the library path for your Python system as a whole or adding it into the source tree of a project that is not meant to be compatible to both Python 3 and Python 2 or older, you can just take either raise3.py or raise2.py and save it as raise_.py.
  2. If you’re adding it into the source tree of a project that should work with both Python 3 and Python 2 and older, copy the whole raise_ directory.

Both of these methods have the advantage that your code can just do from raise_ import raise_ and it’ll just work consistently, without version-detecting boilerplate or hardcoding the version number in the module name (which is an implementation detail).

You are of course welcome to just copy-paste the tiny raise_ function definition into your code, just keep in mind the compatibility issues involved: your code will only work without modification on Python versions compatible with the version you chose, and Python 2’s version causes a SyntaxError in Python 3, which is uncatchable within the same file.

Design Decisions

  • We allow exception to be either an instance or a type, because this convention is very ingrained in Python.

  • We do not currently implement an equivalent to Python 3’s except ... from ... syntax.

    Ultimately, this syntax just assigns one exception as an attribute on another exception.

    This strikes me as complecting two different jobs together: raising an exception instance and initializing an exception instance with a __cause__ attribute.

    I note that generators’ throw method does not have support for a separe “from”/”cause” argument either. Perhaps it should, but then everything implementing this interface would have to implement extra logic to handle that extra argument.

    Instead I would advocate for a separate interface for setting the __cause__ or __context__ attributes on exceptions, such as extending BaseException with with_cause and with_context methods.

  • We do not use the convention of taking separate type and value arguments because it seems like a counter-intuitive and inappropriate convention for raising an exception.

    Python 3 dropped support for separate type and value from the raise statement, so it seems enough people responsible for the language already agree with this assessment.

    Also fully/properly supporting all semantics/variations that raise allowed before Python 3 would bloat the code excessively.

  • We do not support Python 3’s __traceback__ behavior: we do not try to emulate it in Python 2 and we intentionally suppress Python 3’s automatic implicit use of __traceback__ when raising, because:

    • When an insufficiently careful coder (almost all of us almost all of the time) has code work one way on one platform, they assume it will work that way consistently on other platforms.

    • Emulating Python 3’s behavior on Python 2 creates extra potential for wrong behavior: a native except called between code that uses the emulation will result in references to stale traceback objects on the exception being used.

    • The following two mantras feel like useful heuristics here:

      Perfection is reached not when there’s nothing left to add, but when there is nothing left to take away.

      and

      It is far easier to introduce a feature than to remove one.

    • I want to emphasize this again because it’s a lesson I learned from the portability hellscapes of Bourne shell and C: if it differs among implementations it will be the source of bugs and pain.

  • We use two separate implementation files and an __init__.py that imports one or the other because this avoids using exec.

    We want to avoid using exec because it brings its own slew of portability problems, because it makes the code messier (nesting code in strings), and because I wanted the implementations for each version of the language to be independently reusable from a trivial copy.

  • We use a raise_ package directory and __init__.py because it makes setup.py and pip install stupid simple rather than trying to figure out a way to only install the right file as raise_.py.

    While I would love to implement it so that a pip install from Python 3 only installed raise3.py as raise_.py, ditto for 2, this would make the packaging stuff far less trivial.

  • __init__.py tries BaseException.with_traceback and uses AttributeError to fail instead of import raise_.raise2 and SyntaxError to fail because it conceptually highlights the primacy of Python 3 as the ought-to-be-default case.

    I also think it’s conceptually cleaner to not waste Python’s time parsing and interpreting a file only for it to abort on a syntax error. Performance-wise it’s negligible and thus a non-issue though.

    Sadly this breaks pylint on Python 3, because it unconditionally imports the raise2 and aborts upon getting the syntax error. But on a tiny module like this, that’s not a major issue. I manually worked around it to run pylint by commenting out the offending import, and I don’t foresee enough changes to make that a hassle.

  • We don’t do anything about flake8 complaining that __version__ is imported but not used because this module is too tiny for me to justify throwing in some linter-specific disabling comment just to quell one spurious warning in an otherwise flake8-silent file.

  • We do not allow exception or traceback to be arbitrary callables: Even though it has value for all/most arguments of all/many functions, it is precisely because of this that it is best implemented as a general composable tool (such a as a decorator/wrapper function).

    If done, it ought to be done for both exception and traceback, so not supporting it for one implies not supporting it for the other.

    Not supporting it is reason to not accidentally let it work despite being undocumented, because again, people assume that if it works it is supported.

    This is why the code uses an affirmative result from issubtype to decide whether to call exception to construct an instance, instead of any other approach, even though this forces calling isinstance first to avoid a spurious TypeError.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Filename, size & hash SHA256 hash help File type Python version Upload date
raise-1.1.0-py2.py3-none-any.whl (12.9 kB) Copy SHA256 hash SHA256 Wheel py2.py3
raise-1.1.0.tar.gz (7.4 kB) Copy SHA256 hash SHA256 Source None

Supported by

Elastic Elastic Search Pingdom Pingdom Monitoring Google Google BigQuery Sentry Sentry Error logging AWS AWS Cloud computing DataDog DataDog Monitoring Fastly Fastly CDN SignalFx SignalFx Supporter DigiCert DigiCert EV certificate StatusPage StatusPage Status page