Django Signals - A Complete Handbook

Django Signals - A Complete Handbook

A Complete Reference on Django Signals - Beyond the Basic

Photo by Mounish Raja on Unsplash

Introduction To Django Signals

The official Django documentation defines signals as…

Django includes a “signal dispatcher” which helps decoupled applications get notified when actions occur elsewhere in the framework. In a nutshell, signals allow certain senders to notify a set of receivers that some action has taken place. ~ Django Signals

You did not get that, right? I know bookish definitions are not fun. Let's learn Django signals through a simple story! Imagine you've finally decided to confess your feelings to your college crush. You write a love letter and ask your friend to deliver it. Your friend does their job, handing the letter to your crush. Now, you wait nervously - will she say yes or no?

This love story might seem unrelated here, but it explains how Django signals work! You're the sender, your love letter is the message, and your friend is the messenger. Your crush is the receiver, and when she gets the message (your letter), she can do whatever she wants (say yes, say no, write back...whatever).

Just like in our story, Django signals involve senders, messages, and receivers. When something happens (an event), a sender sends a message. Receivers listen to these messages, and when they hear one, they can take action. This means B (a receiver) can be notified that something X has happened by A (the sender), and then B can do whatever it wants based on that information.

Think of it as a built-in communication system within your app. Someone shouts out "Hey, something big happened!", and anyone listening can chime in and do their part. That's the magic of Django signals: a simple secret messaging service for automated actions within your app!

And remember, senders can be any object or function, and receivers can be any Python function or method. This makes signals incredibly flexible and powerful tools for your Django projects.

Why Use Signals - Use Cases

Assume Mr. X, is a shop owner from your village. He throws seasonal sale bonanzas whenever winter, summer, or a festive season rolls around. It's like an unspoken rule in his shop: "If it's a special event, discounts get triggered!"

In the world of Django, these "rules" translate to Signals. Think of them as a powerful messaging system inside your app. Whenever a specific event happens (like Mr. X noticing winter has arrived), a message gets sent out. And just like Mr. X's staff, who jump into action preparing sale posters after they have received the message from Mr. X that winter has arrived, receiver functions in your code can listen for these messages from the sender and perform custom tasks.

Django comes with plenty of built-in "events" ready to trigger signals, like user registration, model updates, or even HTTP requests. You can also create your custom signals for any specific need. More on this later.

For instance, imagine you have two separate Django apps: one for authentication and another for promotions. When a user signs up (event), you could use a signal to trigger the promotions app to send a welcome email (action). No direct dependencies are needed!

Or, consider user photos: uploading one could automatically trigger a signal, prompting another app to add a watermark and generate thumbnails (multiple actions, anyway). This keeps your photo uploading code clean and focused.

Signals can also be used for logging, and keeping an eye on admin activity in your core project. Imagine a dedicated app logging every button click and change made by admins, without direct intervention in the core project.

The general idea behind Django signals, you can assume as this straightforward - "If this happens, then do this" or “If this happens, then that should also happen “.

Remember, while signals offer loose coupling, overusing them can turn your code into a tangled mess. Careful documentation and knowing when a direct function call might be simpler are key to keeping your app sleek and maintainable.

Anatomy of Django Signals

To grasp how Django signals work, let's dissect a real-world example. Imagine the user_logged_in signal, which is triggered whenever a user successfully logs into your Django project. Here's a code snippet that demonstrates how to tap into this signal:

# signals.py 

from django.contrib.auth.signals import user_logged_in
from django.dispatch import receiver

@receiver(user_logged_in, sender=User)
def user_login_receiver(sender, request, user, **kwargs):
    # perform any task that you want as a user has logged into the project. 
    # you may perform anything, and any code here will be automatically executed 
    # when the user logs in thanks to the "user_logged_in" built-in signal.

Let's break down this code step by step:

user_login_receiver(): This is our custom receiver function. We want to listen to the user who is logging in to the system and perform some specific task at this time.

Think of it as your college crush to whom you have sent your love letter - the receiver of the love letter.

@receiver: This is a decorator and with this, we can make a method or function to listen to any signal. This decorator is making the connection between our custom receiver function and the user_logged_in signal.

Think of the receiver() as your friend, who delivered your love letter to your crush - making a bridge between you and your crush.

@receiver(user_logged_in): user_logged_in is a built-in signal in Django triggered when a user logs in. The user_login_reciever() listens to this signal using the receiver() decorator to listen to the event (if a user has logged in).

Think of user_logged_in as the love letter i.e. the actual signal - the love letter is the signal to your crush that you like her, right?

@receiver(user_logged_in, sender=User): Here, the sender is the class where the incident occurred. The sender is the part of the code you're interested in noticing when any changes happen, and then you want to perform something automatically. The sender can be any class or object.

In the analogy, you are the sender, who is sending the signal of love to your crush. You are the sender, sending the signal (your love letter) through your friend (the decorator) to your crush (the receiver).

def user_login_receiver(sender, request, user, **kwargs): This is our custom signal receiver. The sender is the class where something happened, the request is the current request object, the user is the logged-in user, and **kwargs is the wildcard argument. All signals are objects of the django.dispatch.Signal class. This receiver is automatically triggered whenever something occurs to the event it's listening to through the receiver. It allows us to perform any task for a specific reason without being directly involved in that code.

Remember that the scope of the signals is just like the middleware in Django. They can be accessed anywhere in your code.

Activate Django Signal

Signals are batteries of Django. To use them, we need to activate them. Here are the steps to follow to activate the Django Signals:

  1. Create a separate signals.py file in the app you want to have your signals.

  2. In the app.py file, override the ready() method in the AppConfig class, and import the signals.py file.

    Here, remember the child class of AppConfig, as we will need it in your app's __init__.py file.

    For example, for my demonstration, the child class of AppConfig is MyAppConfig.

     # app.py 
     # Activate Signals 
    
     from django.apps import AppConfig
    
     class MyAppConfig(AppConfig):
         default_auto_field = 'django.db.models.BigAutoField'
         name = 'my_app'
    
         def ready(self): # override the ready() method | just create it. 
             import my_app.signals  # import the signals module
    
  3. Go to the __init__.py file of the app, and write in the below format:

    default_app_config = 'your_app_name.apps.the_child_class_name_of_AppConfig’

    for the demonstration I have given, it would be like below-

    default_app_config = 'my_app.apps.MyAppConfig’

You can ignore this if you have installed your app in settings.py like this your_app_name.apps.the_child_class_name_of_AppConfig. If you have installed your app just like this ‘my_app’, then you need to add this in the __init__.py file.

Types Of Django Signals

By Definition

By definition, Django signals are sync and async. From the Django 5.0, async support has been added.

By Availability

By availability, Django signals are of two types - built-in and customs signals. Django has provided many built-in signals for the most important scenarios we might encounter for a need of a singnal. If that does not fit your specific need, you can always create custom signals. I have provided all the built-in signals below. All the signals take the sender and \*kwargs* parameters mandatorily.

For all the signals, register your custom receiver function with the receiver decorator to the signal you want to listen to. They all work in a similar fashion described in the Anatomy of Django Signals section.

Built-In Signals

  1. Built-In Authentication Signals

    There are three authentication-related signals provided by Django.

    import the signals form here - django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed

    • user_logged_in(sender, **kwargs): This signal is already discussed in the Anatomy of Django Singnal section.

    • user_logged_out(sender, **kwargs): Triggered whenever a user is logged out from the system.

    • user_login_failed(sender, **kwargs): Triggered whenever a user login failed. The keyword argument will have the username as plain text, but the password will be hashed.

  2. Built-In Database Signals

    Django has a few database-related signals.

    Import them from here: django.db.models.signals import pre_init, pre_save, pre_delete, post_init, post_save, post_delete, pre_migrate, post_migrate, m2m_changed, class_prepared

    • pre_init(sender, **kwargs): This signal is triggered just before a Django model instance is going to be created. It is activated before the __init__ method of the model class.

    • post_init(sender, instance, **kwargs): post_init is triggered just after the model instance has been created. This signal passes the actual instance that has been created.

    • pre_save(sender, instance, **kwargs): If we need to perform any task before data is saved into the database, we can use the pre_save signal. pre_save is triggered just before .save() method is called.

    • post_save(sender, instance, created, **kwargs): post_save is one of the most used signals in Django. post_save is triggered just after the .save() method is called. As we know that .save() can update or create an instance, post_save signal also provides another boolean argument created. This specifies if the instance is created or updated.

    • pre_delete(sender, instance, **kwargs): We know the .delete() method is responsible for deleting any instance from the Django model. pre_delete is activated just before the .delete() method. At this time, the instance is available in the database.

    • post_delete(sender, instance, **kwargs): post_delete is triggered just after the .delete() method has executed. At this moment, the instance is deleted from the database, but we can access the instance attributes in the code itself - but deleted from the database.

    • pre_migrate(sender, **kwargs): This signal is sent by the migrate command just before the command starts to install the apps.

    • post_migrate(sender, **kwargs): As the name suggests, this signal is sent just after the migration is completed. A receiver that listens to this signal should not try to alter the database schema as this is an inconsistent state for the database and doing so would cause undefined behavior.

There are another two signals m2m_changed and class_prepared available respectively. Whenever a many-to-many relationship is changed, m2m_changed is triggered, and whenever a Django model is registered in the model system, the latter one is activated. These are used by Django internally.

  1. Built-In HTTP Signals

    Django also provides HTTP-related signals.

    Import them from here: django.core.signals import request_started, request_finished, got_request_exception

    • request_started(sender, **kwargs): Activated before Django begins processing an HTTP request. Here the sender is either WsgiHandler or AsgiHander.

    • request_finished(sender, **kwargs): As the name suggests, this signal is triggered when Django finishes processing an HTTP request and returns a response.

    • got_request_exception(sender, **kwargs): Triggers when an exception occurs while processing the HTTP request. This signal is frequently triggered during the middleware phase of an HTTP request.

  2. Built-In Database Wrapper Signals

    Signals when a database wrapper makes a connection to the database.

    Import from here: django.db.backends.signals import connection_created

    • onnection_created(sender, **kwargs) This signal is sent when the database wrapper makes the initial connection to the database, used internally by Django.
  3. Built-In Test Signals

    Django’s test-related signals.

    Import from here: django.test.signals import setting_changed

    • setting_changed(sender, dispatch_uid, **kwargs): This signal is sent when the value of a setting is changed in a testing environment. In a testing environment, the ready() method may run multiple times, to prevent this, we may use dispatch_uid (commonly a string) so that the receiver function is not activated more than once.

Custom Signals

To create custom signals, we need to instantiate the django.dispatch import Signal class. This instance is our custom signal, and we can create a receiver function to listen to it. Although we would send the signal from a specific event, like the user has visited to profile, or the user has sent the admin an email, etc.

Assume we want to

create a custom signal when a user visits his analytics page, if the user is trying to visit the analytics page, we will update his analytics. This would save us from redundant updates of analytics if a user does not want to see them, right? Let’s build our custom signal!

Remember that to use custom signals too, you need to activate them as described in the Activate Django Signal section.

Step 01 - Instantiate the Signal Class

The first step towards creating our custom signal is to just instantiate the Signal class.

#signals.py

from django.dispatch import Signal, receiver

# We just need to instantiate Signal class 
# Yes! That's it! We have created our custom signal!

update_user_analytics_signal = Signal()

Step 02 - Create a Receiver/Listener for Our Custom Signal

Here, we have created a listener/receiver function for our custom signal. We can skip the sender=sender part in the receiver decorator in this case.

#signals.py

@receiver(update_user_analytics_signal)
def update_user_analytics_signal(sender, **kwargs):
    # perform your task to update the analytics page. 
    username = kwargs.get('username')
    ...

Step 03 - Send the Signal

From any part of your program, where you want to send the signal, trigger the signal - the code you have written in the receiver function of this signal will be automatically triggered.

# views.py 

# import the signals module 
from . import signals

def user_analytics_page(request): 
    # All your logic for the analytics page 
    # As the user is trying to access the analytics page, we are sending a signal 
    # So that we can now update the analytics page using the signal. 

    if request.user.is_authenticated: 
       username = request.user.username

        # sending the signal     
        # We can pass any kwargs in the send method (key=value) and this will be received in the 
        # receiver function's kwargs argument. 
        signals.user_notification_signal.send(sender=request.user, request=request, username=username)

        return render(request, 'my_app/user_analytics_page.html', {'username' : username})
    else: 
        return HttpResponseRedirect('/login/')

Conclusion

That's a wrap on Django signals! Remember, they're all about "if this happens, then do that". And with that flexibility comes powerful decoupling—think of it as sending secret messages across your codebase! But even powerful tools need responsible use. Remember, overuse can lead to debugging nightmares, so choose your signals wisely. And don't forget to document those custom signals—future you will thank you.

Remember how we compared signals to a secret messaging system? Well, the world of coding also has its hidden secrets, did you know there are more programming languages than countries in the world? Over 700 ways to talk to a computer—talk about a diverse crowd! Or maybe something a little more interactive? Ever wondered why those CAPTCHA tests are there? The CAPTCHA test you encounter on websites is, in fact, a Turing test in disguise! Alan Turing, a mathematician and computer scientist, proposed the Turing test as a means to determine if a machine could exhibit intelligent behavior! By solving CAPTCHAs, you're unknowingly helping to improve AI technology!

Hope you've enjoyed this journey into the world of Django signals. If you have any questions or ideas, drop them in the comments below! Till then, let me cook another interesting story for you!

Did you find this article valuable?

Support The Backend Diaries by becoming a sponsor. Any amount is appreciated!