Django’s Hooks and Signals

Hernán Tylim
Django Musings
Published in
3 min readApr 15, 2016

Django doc here.

Sometimes you want to do something in your webapp as a response to an event that occurred elsewhere.

For instance. You might want that each time a model is saved to execute an action or update a different model.

You could refactor that logic in a method:

class Book(models.Model):
def increment_times_read(self):
book.times_read += 1
book.author.times_read += 1

So every time that you need to increment times_read you use that method.

b = Book.objects.get(id=123)
b.increment_times_read()

But what happens if somewhere along the line we forget about the method and we change the field manually?

b = Book.objects.get(id=123)
b.times_read = 3 # we do it manually

If Author.times_read needs to have a corresponding value in Author then we are in a problem. (Yes this is a silly example and I am forcing the situation)

The way that you could make this happen automatically and also is clear is using hooks and signals (arrrhh matey -sorry, couldn’t avoid it)

Hooks and Signals

Django provides system signals that announce that something has happened in their system. One such signal is that a model has been saved.

The list of system signal is here.

Also Django provides this framework for your app to create its own signals if you chose it so.

Hooks are the callables that you want to execute when a specified signal happened.

In this example we could say to Django to execute a method on_book_saved each time it saves a book so we could do what we need there.

How to register a hook

Django provides the receiver annotation to easily register automatically you do it this way:

# app/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.db import transaction
from .models import Book@receiver(post_save, sender=Book)
def post_save_handler(sender, **kwargs):
book = kwargs.get(‘instance’, None)
created = kwargs.get(‘created’, False)
raw = kwargs.get(‘raw’, False)

When the annotation “@receiver” is executed post_save_handler will be added automatically to the list of hooks to execute on such signal.

But of course that leaves us another problem. How can we force that annotation to be executed. Annotations will get executed by the interpreter when it loads the module. So how can we force to load this module at the start of our run?

side note: It is customary to put all the hooks and signal declarations in a module called signals.py, you could choose something else but try to avoid coupling your signal logic with your model logic.

The AppConfig.ready() method

When django starts it looks for the AppConfig() instance of your application and executes its ready() method.

In this ready() method is where you do all your initialization.

This is how you implement your AppConfig.ready()

# myapp/apps.py
from django.apps import AppConfig
class MyAppConfig(AppConfig):
name = “myapp”
label = “myapp”
verbose_name = “My App”
def ready(self):
# importing signal handlers
import myapp.signals

So, you see. You subclass AppConfig and implement what you need. Doc here.

note: you need to the very least implement those values of name and label. If you don’t you will get an error when you start your app.

Now that you implemented MyAppConfig you need to tell Django that that AppConfig is the one that represents your application. (if not it will use a default one that it has)

To do that in settings in INSTALLED_APPS you do include the path of that class:

INSTALLED_APPS = [
'django.contrib.admin',
...
'myapp.apps.MyAppConfig']

Alternatively you could (and you should if you are using celery, but is for another article) overwrite default_app_config in your apps’ _init_.py

# myapp/_init_.py
default_app_config = "myapp.apps.MyAppConfig"

And then your settings files remains unchanged:

INSTALLED_APPS = [
‘django.contrib.admin’,

‘myapp’]

--

--