Django Signals- master pre_save and post_save

Gautam Rajeev Singh
7 min readAug 2, 2020

--

Django, with it’s experienced developer community and regular updates have become a reliable framework to build stable and maintainable websites. Not to forget the easy-to-understand the documentation it provides, makes it easier for the beginners to get their way started in Django

Without wasting much time on what Django is, let’s jump right into one of it’s most crucial topics- Signals

What are Signals

Signals, as the name suggests, allow applications to get notified when a certain event occurs. Suppose you want to notify the author of an article whenever someone comments or reacts on the article, but there are several places in your codebase to comment on an article or react on the article. How do you do it? You guessed it right, using signals. Signal hooks some piece of code to be executed as soon as a specific model’s save method is triggered.

To have a clearer idea of the above example, let’s define the models.

### models.py file
from django.db.models import models
from django.contrib.auth.models import User

# Article modol
class Article (models.Model):
title = models.CharField(max_length=50)
body = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)

# Comments model
class ArticleComments (models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
article = models.ForeignKey(Article, on_delete=models.CASCADE)
comment = models.TextField()

# Rating model
class ArticleReaction (models.Model):
CHOICES = (
('1', 'Like'),
('2', 'Love'),
('3', 'Wow'),
('4', 'Hug'),
('5', 'Sad'),
('6', 'Angry')
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
article = models.ForeignKey (Article, on_delete=models.CASCADE)
reaction = models.ChoiceField(choices=CHOICES, default='1', max_length=10)

When to use Signals

Signals are best used when multiple pieces of code are interested in the same model instance events. To understand model instance events in simpler terms, it means a row creation, updating or deletion events.

How to use Signals

While there are many ways to use signals, my favourite way is to use the ‘@receiver’ decorator.

Here is an example -

### signals.py file
from django.db.models. signals import post_save
from django.dispatch import receiver
from .models import ArticleComments

@receiver(post_save, sender=ArticleComments)
def notify_author(sender, instance, created, ** kwargs) :
if created:
# call your function to notify instance.article.author

This might seem a bit confusing right now if you are a beginner but let me help you out here.

The receiver decorator her tells the code that the function(‘notify_user’ here) is about to receive a signal(post_save signal in this case). Now there are many types of Django signals, post_save(django.db.models.signals.post_save) is one of it. Let’s go through some of the important signals separately.

1. Tips to use post_save

As the name suggests, it is called just after the Django model save() function has done its job. The sender parameter here defines the Model from which we are trying to receive the signal. So now if I am to convert the above python code in English, I would say:

Call the function ‘notifiy_author’ after the istance of ‘ArticleComments’ is saved

Whenever we use signals, we get to use some variables. One of those variables, as you can see in the above code snippet is ‘instance’. This is nothing but the instance of the model ArticleComments which is being saved. So, if I want to fetch the author of the comment, I can easily use:

# Fetch data from article instance
instance.article.author
# Parameters used in post save
def notify_author(sender, instance, created, **kwargs):

Now a perk about using post_save signal is that it provides us with a variable named created. This is a flag, which returns True if this post_save signal is called when a new instance (or row) of a model was created.

Following points should be kept in mind while using post_save:
1. There are no special signals like pre_create or post_create. Whenever we call Model function create(), it calls the save signals.
2. You cannot modify the value of the any instance’s fields(for ex: ‘instance.comment’ here) inside post_save, without calling Django save() method again, which is not(never) a good practice to call inside a signal of the same model(Can you guess why?)

2. Use pre_save like a pro

Pre_save (django.db.models.signals.pre_save) is provoked just before the model save() method is called, or you could say model save method is called only after pre_save is called and done its job free of errors.

We know how to notify the author when a new comment is posted(the same way you can create a signal on the creation of ArticleReaction too), but what if I want to notify the author when the reaction is modified? pre_save comes in handy for similar cases.

### signals.py file
from django.db.models. signals import pre_save
from django.dispatch import receiver
from .models import ArticleReaction

@receiver(pre_save, sender=ArticleReaction)
def notify_author_on_reaction(sender, instance, ** kwargs) :

# If instance/row is being created, then do nothing
if instance.id is None:
pass

# Else if it is being modified
else:
current = instance
previous = ArticleReaction.objects.get(id=instance.id)

# If previous reaction is not equal to the current reaction
if previous. reaction != current. reaction:
# notify instance.article.author

We do not get ‘created’ variable to use in pre_save. No worries, we have a workaround for that too.

All the model instances(or rows) have an auto-generated primary key which is name ‘id’. This id is numerically incremented as instances of the model are created. But the ‘id’ can only be generated/assigned after the creation of the row. Hence in pre_save, if we try to access the instance.id before the instance(or row) is created, it will return us None. So the condition on line 10 in the above snippet is nothing but a replica of ‘created’ from post_save.

# The if condition in pre save to check if the instance is being created, 
# i.e. if a new row is being created
if instance.id is None:

(Notice how I am using ‘row’ to refer an instance of a model. It will help you to have a clearer imagination of what’s happening behind the scenes. Similarly, attributes/fields can be referred with another term ‘column’)

Our problem statement was to notify the author if the reaction is changed. Let’s try to backtrace it. To find if the reaction was changed, we need the current reaction and the previous reaction. If the current reaction and the previous reaction does not match, then it concludes that the reaction was changed and hence we need to notify the author about it. But how exactly can we get the previous and the current reactions? To understand that, we have to know one VERY IMPORTANT property about the variable instance in post_save and pre_save:

The post_save’s instance has the attributes with values which are already saved in your model, but the pre_save’s instance has the attributes with values which are yet to be saved in your model.

To make the above line more clearly, let’s take an example. Some user named ‘Ravi’ has already reacted with a ‘Like’. Ravi has a habit of reading everything twice. After reading the article again, he decided to change his reaction to ‘Love’.

Let’s dive back inside our code again. Pre_save is called as soon as Ravi changed the reaction. Since the instance(or row) was already created, we move directly to the else condition in Line 14. Using the above-highlighted property, we can say that the variable ‘instance’ will have the new value ‘Love’ for its attribute ‘reaction’. Let’s name this modified variable ‘instance’ as ‘created’ (Line 15). Now we just need to get the old reaction. Remember that the ‘Love’ reaction has not been saved in our model yet(since we are still in pre_save). So what will we get if we try to get a model instance of ArticleReaction with the id provided by pre_save? We will get the instance with the reaction ‘Like’. Now let’s name this as ‘previous’(line 16). As we now have both old and new instances, we can easily compare these reactions and code are conditions accordingly.

# Line 15 and 16
current = instance
previous = ArticleReaction.objects.get(id=instance.id)

Since pre_save is called right before the model save() method, you can also modify the instance’s fields as per the requirement. For example, if we are using pre_save for ArticleComments and we want to check and remove any abuse words from comments before it is saved in our model, we can do it like this:

if is_abuse(instance.comment):
instance.comment = remove_abusive(instance.comment)

Assuming that is_abusive and remove_abusive is your custom method. Also notice that we are not calling instance.save() after changing the instance.comment. Can you tell why?

3. Other Signals

There many more signals you can use, which you can find here, but this blog was just an attempt to give you the idea of how you can use signals effectively.

Before we finish

Even though signals come in handy when you want to perform actions behind the scenes, you have to be very careful about how you use it.

  1. Do not compromise speed: putting too much load on pre_save or post_save signals might make the model save() method slow.
  2. Lost in the loop: Calling the save() method for the sender model inside the post_save or pre_save will keep calling the signals repetitively.
  3. Remember the use cases: We use post_save when we are interested more in the creation of a model instance without modifying the values, while we use pre_save when we are more into the monitoring the change in model instance’s value, or if we are into modifying the instance’s attribute’s values ourselves.
  4. update() and save signals don’t get along: Django model ‘update()’ method does not invoke any kind of pre_save or post_save signals.

--

--