Understanding Signals in Django

Share this article

Understanding Signals in Django

Signals are a communication mechanism between different parts of a Django project that enable components to send notifications to trigger actions in response to events. In this tutorial, we’ll walk through the basics of Django signals, covering how they enable maintenance of modular and scalable codebase, exploring some of the built-in signals, and looking at how we can define custom signals.

More often than not, a Django project will have more than one app. For instance, in an ecommerce project we might have an app for user management, an orders app, a products app, a payments app, and so on. This way, each app will be solely focused on a specific function.

But at the end of the day, all these apps should work in concert to make the ecommerce project a whole. One or more apps might be interested in knowing when a particular event takes place in another app. For example, the product app might be interested in knowing when a user confirms an order — which will enable the product app to update the inventory.

But how will the product app know when a user places an order, given that they are two separate applications? To solve this, the Django framework uses a signal dispatcher. With this framework, the orders app will send a signal once an order has been saved, and the products app will update the inventory accordingly upon receiving of this information.

So the signaling framework allows for the applications in one project to be decoupled, enabling them to communicate with each other without being tightly dependent.

Table of Contents

Key Takeaways

  1. Overview of Django Signals: Django signals provide a way for decoupled applications to receive notifications when certain actions or events occur. This article explains how signals enable communication between different parts of a Django application, such as the orders and products apps in an ecommerce project.
  2. Working Mechanism of Signals: Signals in Django follow a publisher-subscriber pattern, with signal senders acting as publishers and receivers as subscribers. This article describes how signals are set up and used, including the creation of custom signals and the connection of signals to receivers.
  3. Practical Applications of Django Signals: The tutorial includes practical examples to demonstrate the usage of signals in Django. These include scenarios like updating inventory upon order confirmation and automatic creation of customer profiles, showcasing the versatility and utility of Django signals in real-world applications.

Understanding Signals

Signals in Django are a notification system that allows certain “senders” to notify a set of “receivers” when certain actions take place. They help decoupled applications get notified when particular actions or events take place elsewhere in the framework. In the context of our example, the orders app will “send” a signal upon the confirmation of an order, and since the products app is interested in this event, it will have registered to “receive” it, and then it will take some action upon the receipt.

How signals work in Django

Signals work similarly to the pub–sub pattern — where the sender of the signal is the publisher and the receiver of the signal is the subscriber. This means that, for a receiver to receive a signal, it must have subscribed (in this case registered) to receive the signal.

Signal senders and receivers

A signal sender is any Python object that emits a signal, while a receiver is any Python function or method that gets executed in response to the signal being sent. It’s also important to note that some signals — especially the built-in signals — are always sent out whether there’s a registered receiver or not. In our example, the orders app will be the sender of the signal and the products app will be the receiver of the signal.

Setting Up a Django Project

To demonstrate how signals work, let’s implement a sample project by following the steps laid out below.

Create the project directory

mkdir my_shop

This will create a directory named my_shop that will hold the ecommerce project.

Create and activate a virtual environment

A virtual environment is an isolated Python environment that allows us to install the dependencies of a project without affecting the global Python environment. This means we can have different projects running in one machine, each with its own dependencies and possibly running on different versions of Django.

To create a virtual environment, we’ll use the virtualenv package. It’s not part of the standard Python library, so install it with the following command:

pip install virtualenv

Upon installation of the package, to create a virtual environment for this project we have to get into the my_shop directory:

cd my_shop

Create a virtual environment using the following command:

virtualenv venv

The above command creates a virtual environment named venv. Having created it, we have to activate it to use it.

For Linux/macOS:

. venv/bin/activate 

For Windows:

. venv\Scripts\activate

Install Django and other dependencies

Once the virtual environment is active, we can install Django and other dependencies the project might need, but for this demonstration we just need Django:

pip install Django

Creating the project

Upon successful installation of Django, create a project and name it my_shop similarly to the project holder we created above:

django-admin startproject my_shop .

The above command creates a Django project named my_shop. The dot at the end indicates that we wish to create the project in the current directory, without creating extra directories.

Creating the individual apps

We’ll now create two apps — the products app and the orders app. We’ll first create the products app with the default files for any Django app:

python manage.py startapp products

Add the products app to the installed apps of the project, open the my_shop directory, then go to the settings.py file and go to the INSTALLED_APPS setting, adding the following line of code at the bottom:

INSTALLED_APPS = [
    # other installed apps
    'products.apps.ProductsConfig',
]

That setting registers the products app with the project and enables us to run migrations that will create the database tables.

Then create the orders app:

python manage.py startapp orders

As we did for the products app, we add the orders app to the INSTALLED_APPS setting too. Open the settings.py file and add the following line of code at the bottom:

INSTALLED_APPS = [
    # other installed apps
    'orders.apps.OrdersConfig',
]

Defining the models for the apps

This demonstration will involve changing and updating some values in the database, to indicate some state changes, which eventually leads to some events taking place, and for that we’ll need to define models for the two apps. Let’s define the models for the products app first. Open the products app and go to the models.py file and put in the following block of code:

# products/models.py

from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField()
    price = models.DecimalField(max_digits=10, decimal_places=2)
    quantity = models.PositiveIntegerField(default=0)

    def __str__(self):
        return self.name

The code above defines a product model that will be mapped to a products table in the database — with a few fields that are quite descriptive.

Next define the orders models. Open the orders app and put in the following code to the models.py file:

# orders/models.py

from django.db import models
from products.models import Product

class Order(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    quantity = models.PositiveIntegerField()
    total_price = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)
    confirmed = models.BooleanField(default=False)

    def save(self, *args, **kwargs):
        # Calculate total_price before saving the order
        self.total_price = self.product.price * self.quantity
        super().save(*args, **kwargs)

    def __str__(self):
        return f"{self.quantity} x {self.product.name}"

Having defined the models, we have to run migrations. This will create the tables in the database. The Django ORM avails various database management commands, but for now we’ll use the two that are used to prepare the database tables.

Up to this point, we haven’t run the project to test if it’s setup correctly. Before running the migrations to create the two new models, let’s run the project using the following command:

python manage.py runserver

The above command fires up the development server, and if everything is set up correctly, we should have an output similar to the one below:

Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
January 15, 2024 - 11:22:19
Django version 5.0.1, using settings 'sitepoint_django_signals_tutorial.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

If we have similar output, that means the project is configured properly and we have a basic Django project running. If an error does occur, I recommend heading over to the community to get some suggestions about how to deal with them.

While still on the output, let’s note the fourth line that starts with, “You have 18 unapplied migrations”. If we choose to use a database for our Django project, being a batteries-included framework, Django will create some tables meant for management, and that’s what this warning is about. It means that we haven’t created the Django management tables.

To create them, run the following command:

python manage.py migrate

Running that command creates quite a number of tables. But where are the tables being created? We haven’t set up a database for use in our project yet! Django ships with SQLite3 preconfigured by default. Therefore, we don’t need to do any configuration for our project to work with SQLite.

Now that the Django management tables have been created, we have to create the tables for the products and orders apps. To do that, we’ll need two sets of commands, and this is the format followed every time we want to create a new table for newly defined model. The first command converts or maps our model class definition to the SQL needed to create the database tables, and the command is makemigrations:

python manage.py makemigrations

Running that command without specifying any app creates migrations for all apps. The next thing is to apply the migrations, which eventually creates the tables, using the following command:

python manage.py migrate

This command creates the tables for two apps that we have in this sample application.

Basics of Django Signals

In this section, let’s delve into the fundamentals of Django signals. We’ll explore the steps to follow to get signals working in an application. We’ll do that by making incremental changes in the project we’ve just set up.

Importing necessary modules

To get signals to work in our project, it’s essential to import the required modules. For a start, let’s import the Signal and receiver from the django.dispatch module. The Signal class is used to create a signal instance — especially if we want to create custom signals. In the sample project, in the orders app, we’re only going to import the Signal class, while the receiver module will be imported in the products app. The receiver module avails the receiver decorator, which connects the signal handler to the signal sender.

While the django.dispatch module serves as the core module for defining custom signals, Django offers built-in signals that are accessible through other modules. We’ll cover them in more detail in the built-in signals section.

Using the sample project, let’s see how we could add the signals functionality. The Django documentation recommends that we create a signals.py file that will contain all the code for the signals. In the root of the products and orders app, create a file and name it signals.py.

Creating a signal instance

In the orders app, open the signals.py file and put in the following code:

# orders/signals.py

from django.dispatch import Signal

# create a signal instance for order confirmation
order_confirmed = Signal()

The code above creates an order_confirmed signal that will be sent when an order is confirmed, albeit manually.

Connecting signals in apps.py

So as to make the signal sending and receipt functionality available throughout the lifecycle of the application, we have to connect the signals in the app configuration file. We’re going to do this for both applications.

Open the orders app then go to the apps.py file, update the OrdersConfig class with the following method:

# orders/apps.py

def ready(self):
    import orders.signals

The ready() method is a built-in method of the AppConfig class that’s extended by the configuration classes of the particular app we’re working with. In this particular case, the OrdersConfig extends it, and hence the methods, including ready(), are available for it to extend and override. Therefore, overriding the method in this case sets signals to be sent when the app is fully loaded.

Next step is to connect the signals in the products app. Open it and go the apps.py file and put in the following block of code:

# products/apps.py

def ready(self):
    import products.signals

This addition in both apps ensures that signals will be sent when the request response cycle begins.

Creating a signal sender

Django provides two methods to enable signal sending. We can use Signal.send() or Signal.send_robust(). Their difference is that the send() method does not catch any exceptions raised by receivers.

To send a signal, the method takes the following format:

Signal.send(sender, **kwargs)

Signal is somewhat of a placeholder to mean the sending signal — such as order_confirmed.send() — while the sender argument could be the app sending the signal or a model or some other part of the framework that can send signals. The sender in our example will be the instance of the order that has just been confirmed as sender=order.

Let’s see how to use the order_confirmed signal in our sample application. Since we want to send the signal upon the confirmation of an order, in the view that will handle order processing, that’s where we want send the signal from once a user confirms their order. So open the orders app the views.py and put in the following block of code:

# orders/views.py

from django.shortcuts import get_object_or_404
from django.http import HttpResponse
from django.dispatch import receiver
from orders.models import Order

def confirm_order(request, order_id):
    if request.method == 'POST':
        order = get_object_or_404(Order, id=order_id)

        # Perform order confirmation logic here

        # Send the order_confirmed signal
        # the sender of the signal is the order that just been confirmed
        order_confirmed.send(sender=order)

        return HttpResponse(f"Order {order_id} confirmed successfully.")
    else:
        return HttpResponse("Invalid request method. Use POST to confirm the order.")

Connecting a signal handler (receiver)

A signal handler is a function that gets executed when an associated signal is sent. Django provides two ways of connecting a handler to a signal sender. We can connect the signal manually, or we can use the receiver decorator.

Manual connection

If we choose to do it manually, we can do it this way:

# products/signals.py

from orders.signals import order_confirmed

order_confirmed.connect(<the_signal_handler_function>)

Using the decorator

Using the @receiver decorator, we’re able to associate a signal sender with a particular signal handler. Let’s use this method. Create a function that will update the inventory when the order_confirmed signal is sent. This handler will be in products app. Open the products/signals.py file and put it the following code:

# products/signals.py

from django.dispatch import receiver
from orders.signals import order_confirmed

@receiver(order_confirmed)
def update_quantity_on_order_confirmation(sender, **kwargs):
    """
    Signal handler to update the inventory when an order is confirmed.
    """
    # Retrieve the associated product from the order
    product = sender.product

    # Update the inventory
    product.quantity -= sender.quantity
    product.save()

    print(f"Quantity updated for {product.name}. New quantity: {product.quantity}")

The handler function should always take in a sender and &ast;&ast;kwargs arguments. Django will throw an error if we write the function without the &ast;&ast;kwargs arguments. That is a pre-emptive measure to ensure our handler function is able to handle arguments in future if they arise.

Built-in Signals in Django

Django ships with some built-in signals for various use cases. It has signals sent by the model system; it has signals sent by django-admin; it has signals sent during the request/response cycle; it has signals sent by database wrappers; there are also signals sent when running tests.

Model signals

Model signals are signals sent by the model system. These are signals sent when various events take place or are about to take place in our models. We can access the signals like so: django.db.models.signals.<the_signal_to_use>

pre_save

This signal is sent at the beginning of the model save() method. It sends various arguments with it: the sender is the model class sending the signal, instance is the actual instance being saved, raw is a Boolean value which is true if the model is saved exactly as presented.

post_save

This signal is sent at the end of model save() method. This is a particularly useful signal and it can be used in various cases. For example, in the my_shop project, we can use it to notify the products app upon the confirmation of an order. Similar to the pre_save signal, it also sends some arguments along — the sender, instance, created, raw, using and update_fields. most of these arguments are optional, but understanding the effects of their usage goes a long way to ensuring we use signals correctly in our application.

Request/response signals

Request/response signals are signals that are sent out by the core framework during the request/response cycle. We can access the signals like so: django.core.signals.<the_signal>.

request_started

This signal is sent when Django starts processing a request.

request_finished

This signal is sent when Django finishes sending a HTTP response to a client.

The framework avails quite a number of built-in signals for use. Learn more about them in the signals documentation.

How to use built-in signals in a project

Using built-in signals in a project is not so different from using custom signals. The only difference is that we don’t need to manually initialize sending the signal, since it’s sent out automatically by the framework whether there’s a receiver registered or not. In other words, we don’t have to explicitly call a methods like Signal.send() to trigger built-in signals, as Django takes care of that.

For instance, let’s see how we could utilize the post_save to demonstrate order confirmation and update the inventory. In the products/signal.py file, update the code like so:

# products/signals.py

from django.db.models.signals import post_save
from django.dispatch import receiver
from orders.models import Order

@receiver(post_save, sender=Order)  # Connect to the built-in post_save signal
def update_quantity_on_order_confirmation(sender, instance, created, **kwargs):
    # This function will be called automatically after Order model instance is saved
    if created:
        # Perform actions for newly created instances
        product = instance.product

        # Update the inventory
        product.quantity -= instance.quantity
        product.save()

    else:
        # Perform actions for updated instances

The above code imports the post_save signal from the models.signals. I also imports the receiver module from django.dispatch and finally the Order model from the orders app.

In the @receiver decorator, apart from including the post_save signal as the sender, we also include the model from which we want to listen for signals from. The reason for this is that all models in our project will emit the post_save signal, so we want to be specific as to which model we’re listening to signals from.

In the handler function, note that updating the inventory will only happen if the created option is true. The reason for this is that post_save signal is sent for new order creation instances and updates to orders, so we want to update the inventory when a new order is created.

In the orders app, we’ll update the confirm_order view and do away with the part where we send the signal manually, since the post_save signal will be sent automatically by the Order model class.

Practical Examples

While we’ve seen the usage of signals to confirm an order, there’s quite a number of different ways that signals can be used in our projects, so let’s look at a few examples.

Example 1: Using signals for automatic customer profile creation

To expand on the ecommerce project, we could have an app for account management. This is where we might create various accounts for users in our system where we’d have administrative users, and other users like customers. We could set up our system in such as way that users who sign up to the system in the frontend will be customers, and we could have another way of creating superusers of the system. So in this case, we could also have an app for customer management, but have customers created upon signing up.

This way, the accounts management app will be responsible for accounts creation, and once an account is created for a user, a customer profile will be automatically created for them.

Let’s see an example:

# customers/signals.py

from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from customers.models import Customer

@receiver(post_save, sender=User)
def create_customer_profile(sender, instance, created, **kwargs):
    if created:
        Customer.objects.create(user=instance)

In this code example, a signal is employed to automatically create a customer profile whenever a new user is registered. The create_customer_profile function is connected to the post_save signal for the User model using the @receiver decorator. When a new user instance is created (as indicated by the created flag), the function generates a corresponding customer profile by utilizing the Customer model’s manager to create an instance with a reference to the newly registered user. This approach streamlines the process of associating customer profiles with new user registrations, enhancing the efficiency and maintainability of the application’s user management system.

Example 2: Triggering email notifications with signals

In this use case, we could have a blogging application where the authors are notified through an email when a reader leaves a comment for them on their blog.

Let’s see an example:

# blog/signals.py
# signals.py

from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.mail import send_mail
from blog.models import Comment

@receiver(post_save, sender=Comment)
def send_comment_notification(sender, instance, created, **kwargs):
    if created:
        subject = 'New Comment Notification'
        message = 'A new comment has been posted on your blog.'
        from_email = 'your@example.com'
        recipient_list = [instance.blog.author.email]

        send_mail(subject, message, from_email, recipient_list)

The above code utilizes signals to trigger an email notification when a new comment is posted on a blog. The send_comment_notification function is connected to the post_save signal for the Comment model, ensuring its execution upon each save. The function checks if the comment is newly created (not updated) and, if so, constructs an email notification with a predefined subject and message. The email is sent to the author of the blog to notify them of the new comment. This approach enables automatic and real-time email notifications for blog authors, enhancing user engagement and interaction with the platform.

Conclusion

In this tutorial, we’ve covered signals in Django, what they are and how to use them. We’ve also looked at how to define custom signals, built-in signals and a few real world use cases of signals.

Kabaki AntonyKabaki Antony
View Author

Kabaki is a full-stack software engineer and a technical writer with a passion for creating innovative digital experiences. When not coding, he enjoys sharing knowledge with others by writing technical articles to help developers hone their skills.

djangosignals
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week