TAGS :Viewed: 13 - Published at: a few seconds ago

[ How do I instantiate a specific subclass based on arguments passed to __init__? ]

I'm wrapping a remote XML-based API from python 2.7. The API throws errors by sending along a <statusCode> element as well as a <statusDescription> element. Right now, I catch this condition and raise a single exception type. Something like:

class ApiError(Exception):
    pass

def process_response(response):
    if not response.success:
        raise ApiError(response.statusDescription)

This works fine, except I now want to handle errors in a more sophisticated fashion. Since I have the statusCode element, I would like to raise a specific subclass of ApiError based on the statusCode. Effectively, I want my wrapper to be extended like this:

class ApiError(Exception):
    def __init__(self, description, code):
        # How do I change self to be a different type?
        if code == 123:
            return NotFoundError(description, code)
        elif code == 456:
            return NotWorkingError(description, code)

class NotFoundError(ApiError):
    pass

class NotWorkingError(ApiError):
    pass

def process_response(response):
    if not response.success:
        raise ApiError(response.statusDescription, response.statusCode)

def uses_the_api():
    try:
        response = call_remote_api()
    except NotFoundError, e:
        handle_not_found(e)
    except NotWorkingError, e:
        handle_not_working(e)

The machinery for tying specific statusCode's to specific subclasses is straightforward. But what I want is for that to be buried inside of ApiError somewhere. Specifically, I don't want to change process_response except to pass in the value statusCode.

I've looked at metaclasses, but not sure they help the situation, since __new__ gets write-time arguments, not run-time arguments. Similarly unhelpful is hacking around __init__ since it isn't intended to return an instance. So, how do I instantiate a specific subclass based on arguments passed to __init__?

Answer 1


A factory function is going to be much easier to understand. Use a dictionary to map codes to exception classes:

exceptions = {
    123: NotFoundError,
    456: NotWorkingError,
    # ...
}

def exceptionFactory(description, code):
    return exceptions[code](description, code)

Answer 2


You could create a series of subclasses and use the base class' __new__ as a factory for the children. However, that's probably overkill here; you could just create a simple factory method or class. If you wanted to get fancy in another direction though, you could create a metaclass for the base class that would automatically add your subclasses to a factory when they are created. Something like:

class ApiErrorRegistry(type):

    code_map = {}

    def __new__(cls, name, bases, attrs):

        try:
            mapped_code = attrs.pop('__code__')
        except KeyError:
            if name != 'ApiError':
                raise TypeError('ApiError subclasses must define a __code__.')
            mapped_code = None
        new_class = super(ApiErrorRegistry, cls).__new__(cls, name, bases, attrs)
        if mapped_code is not None:
            ApiErrorRegistry.code_map[mapped_code] = new_class
        return new_class

def build_api_error(description, code):

    try:
        return ApiErrorRegistry.code_map[code](description, code)
    except KeyError:
        raise ValueError('No error for code %s registered.' % code)


class ApiError(Exception):

    __metaclass__ = ApiErrorRegistry


class NotFoundError(ApiError):

    __code__ = 123


class NotWorkingError(ApiError):

    __code__ = 456


def process_response(response):

    if not response.success:
        raise build_api_error(response.statusDescription, response.statusCode)

def uses_the_api():
    try:
        response = call_remote_api()
    except ApiError as e:
        handle_error(e)

Answer 3


Create a function that will yield requested error class basing on description. Something like this:

def get_valid_exception(description, code):
    if code == 123:
        return NotFoundError(description, code)
    elif code == 456:
        return NotWorkingError(description, code)

Depending on your requirements and future changes, you could create exceptions with different arguments or do anything else, without affecting code that uses this function.

Then in your code you can use it like this:

def process_response(response):
    if not response.success:
        raise get_valid_exception(response.statusDescription, response.statusCode)