Doddie Doddie - 1 month ago 3
Python Question

How do I allow a function to be redefined in Django?

I have a 'core' Django product that includes default implementations of common tasks, but I want to allow that implementation to be redefined (or customised if that makes it easier).

For example in the core product, I might have a view which allows a user to click a button to resend 'all notifications':

# in core/
... imports etc...
from core.tasks import resend_notifications

def handle_user_resend_request(request, user_id):
user = get_object_or_404(id=user_id)

if request.method == 'POST':
for follower in user.followers:

... etc etc ...

# in core/
... imports etc...

def resend_notifications(id):

And then in some deployments of this product, perhaps the 'resend_notifications' needs to look like:

# in customer_specific/
... imports etc ...

def resend_notifications(id):
person = User.objects.get(id=id)
if '' in
# This is not a real email, send via the magic portal
# and send via fax for good measure

How do I get the
function in the
file to point to the customer_specific version?

Should I be defining this in the Django config and sharing access that way? What if the tasks are actually Celery tasks?

NB: The tasks I have are actually defined as Celery tasks (I removed this extra detail because I think the question is more general). I have tried with a custom decorator tag that mutates a global object, but that is definitely not the way to go for a number of reasons.

PS: I feel like this is a dependency injection question, but that is not a common thing in Django.


This ended up being solved via a Django settings object that can be reconfigured by the deployment config. It was largely inspired by the technique here: from django-rest-framework.

For example, I have a settings file like this in my project:


Settings for <YOUR PROJECT> are all namespaced in the YOUR_PROJECT config option.
For example your project's config file (usually called `` or '') might look like this:


This module provides the `yourproject_settings` object, that is used
to access settings, checking for user settings first, then falling
back to the defaults.
# This file was effectively borrow from

from __future__ import unicode_literals
from django.conf import settings
from django.utils.module_loading import import_string

    'RESEND_NOTIFICATIONS_TASK': 'core.tasks.resend_notifications',

# List of settings that may be in string import notation.


def perform_import(val, setting_name):
    If the given setting is a string import notation,
    then perform the necessary import or imports.
    if val is None:
        return None
    if callable(val):
        return val
    if isinstance(val, (list, tuple)):
        return [perform_import(item, setting_name) for item in val]

        return import_string(val)
    except (ImportError, AttributeError) as e:
        msg = "Could not import '%s' for setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e)
        raise ImportError(msg)

class YourProjectSettings(object):
    A settings object, that allows settings to be accessed as properties.
    For example:

        from your_project.settings import yourproject_settings as the_settings

    Any setting with string import paths will be automatically resolved
    and return the class, rather than the string literal.
    namespace = 'YOUR_PROJECT'

    def __init__(self, mandatory=None, defaults=None, import_strings=None):
        self.mandatory = mandatory or MANDATORY_SETTINGS
        self.defaults = defaults or DEFAULTS
        self.import_strings = import_strings or IMPORT_STRINGS


    def user_settings(self):
        if not hasattr(self, '_user_settings'):
            self._user_settings = getattr(settings, self.__class__.namespace, {})
        return self._user_settings

    def __getattr__(self, attr):
        if attr not in self.defaults and attr not in self.mandatory:
            raise AttributeError("Invalid Pyrite setting: '%s'" % attr)

            # Check if present in user settings
            val = self.user_settings[attr]
        except KeyError:
            # Fall back to defaults
            val = self.defaults[attr]

        # Coerce import strings into classes
        if attr in self.import_strings:
            val = perform_import(val, attr)

        # Cache the result
        setattr(self, attr, val)
        return val

    def __check_settings(self):
        for setting in self.mandatory:
            if setting not in self.user_settings:
                raise RuntimeError(
                    'The "{}" setting is required as part of the configuration for "{}", but has not been supplied.'.format(
                    setting, self.__class__.namespace))

yourproject_settings = YourProjectSettings(MANDATORY_SETTINGS, DEFAULTS, IMPORT_STRINGS)

This allows me to either:

  • Use the default value (i.e. 'core.tasks.resend_notications'); OR
  • To redefine the binding in my config file:


    ... other django settings like DB / DEBUG / Static files etc
        'RESEND_NOTIFICATIONS_TASK': 'customer_specific.tasks.resend_notifications',
    ... etc. ...

Then in my view function, I access the correct function via the settings:


... imports etc...
from yourproject.settings import yourproject_settings as my_settings

def handle_user_resend_request(request, user_id):
    user = get_object_or_404(id=user_id)

    if request.method == 'POST':
        for follower in user.followers:

    ... etc etc ...