Django comes with many features out of the box, and they’re all ready to use and combat tested. But, on the other hand, Django signals are one of the most underappreciated out-of-the-box capabilities Django offers. Django Signals is a technique for notifying detached applications when particular events occur.
Imagine you want to invalidate a cached page if a particular model instance is updated, but this model is updated in multiple places throughout your codebase. You can do this by using signals and hooking some code to run every time triggered by this model’s save method.
Another famous use case is when you’ve used the Profile technique to expand the Custom Django User through a one-to-one relationship. We commonly utilize a “signal dispatcher” to listen for the User’s post_save event and update the Profile instance simultaneously.
Custom Signals in Django
This article will explore how to develop event-driven, decoupled logic in Django. This kind of logic construction is beneficial when a specific function or block of code must be executed on each occurrence of a particular event. In other words, Signals assist us in triggering the execution of a process or block of code when an event (or Signal) occurs.
Django offers a “signal dispatcher” that allows detached apps to be notified when activities in the framework occur. In a nutshell, signals allow particular senders to inform a group of receivers that an event has occurred. They’re particularly beneficial when many pieces of code are interested in the same circumstances. Further, any Python function or method can be used as a recipient function.
Most Django signals articles will show you how to utilize signals on save or request models. On the other hand, signals are one of Django’s most powerful features. They are also customizable.
The documentation makes it simple to understand what Django signals are and how to utilize built-in signals like post_save, pre_save, etc. However, we have a big challenge when constructing a bespoke signal. We still struggle with implementation on occasion.
Because there are a lot of minor aspects that need to be checked, we will look at our processes while working with Django signals and recommend them to anyone else.
How does it work?
If you’re familiar with the Observer Design Pattern, Django implements it similarly. Or, at the very least, it accomplishes the same goal.
The senders and receivers are the two most essential parts of the signaling system. As the name implies, the transmitter is in charge of sending out a signal, while the receiver is in charge of receiving the Signal and acting on it.
A receiver must be a function or an instance method to receive signals.
The sender must be a Python object or None to receive events from any sender.
The connect(link) method is used to connect the senders and receivers through “signal dispatchers,” which are instances of Signal. However, you’ll always want to utilize the Signal class when creating bespoke signals. ModelSignal is a subclass of Signal in the Django core that allows the sender to be given lazily as a string of the app_label.ModelName form. To receive a signal, use the Signal.connect() method to register a receiver function that will be invoked when the Signal is sent.
When is it appropriate to use Signals in Django?
Before we go any further, here are some examples of when you should use it:
- When a lot of code is interested in the similar events at the same time;
- When working with a detached application, such as a Django core model.
- A model that is defined by a third-party application.
The steps to generate a custom signal in Django are as follows:
– Let’s get started with a Django app.
– Inside the program where you want the Signal, create a signals.py. Note that the name can be anything, but we prefer this naming convention because it makes it easier to recognize. We would advise avoiding all of the model’s built-in signals. Why? Because it’s tough to keep track of who’s listening to what and how things are changed as your codebase grows larger. In addition, always build a custom signal and then fire it from the model’s def save(self) method.
Declaring the Signal Definition
# custom_signals_app/signals.py from django import dispatch code_task_done = dispatch.Signal(providing_args=["code_id"])
We’ve created a custom signal named code_task_done in the codebase above that any application can import and use. However, nothing validates that the Signal offers these arguments to its listeners, so the providing_args option is simply documentation.
To send a signal, you can call either Signal.send_robust() or Signal.send (). It would help if you gave the sender an argument (usually a class) and may offer as many additional keyword arguments as possible.
Let’s imagine you wish to use tasks.py to raise a signal. The latter can be any file in any app. In addition, any other application can likewise import and call the signals. The following syntax is used.
# custom_signals_app/signals.py from application import signals def do_code_task(): # do something now signals.code_task_done.send(sender='python_task_done', code_id=321) # at this point, the sender is anything; the same is the argument.
The handling of exceptions raised by receiver methods differs between send() and send_robust(). Send() does not catch any exceptions thrown by recipients; instead, it permits faults to spread. As a result, in the event of an error, not all receivers may be told of the Signal, whereas send_robust() catches all faults originating from Python’s Exception class and ensures that every receiver is notified of the Signal. If an error happens, the error instance is returned in the tuple pair for the receiver who caused the error.
So far, we’ve learned how to make a signal and use it. However, we have yet to see what occurs if we issue a signal. It is a crucial and challenging aspect. Every time we fire a signal, we need a receiver to listen to it and take action. We’ll need to make a receivers.py file for this. The filename can be anything, but try to keep this as a convention for better readability. It is helpful to listen to Signals in receivers.py so that any signal raised will trigger the function specified here.
# custom_signals_app/signals.py from django.dispatch import receiver from application import signals @receiver(signals.code_task_done) def my_code_done(sender, code_id, **kwargs): print(sender, code_id) # prints ' python_code_done', 321
The receiver decorator subscribes to the code_task_done Signal, and the receiver my_code_done function is called whenever the Signal is dispatched.
The most crucial part is about to begin. Many people overlook this, which makes signaling more challenging. Ensure that the receivers are imported into your apps. It is crucial because we need to tell Django to load the receivers when the app is ready to be linked to the signals framework. It is what we refer to as registering the receiver.py in your app load.
# custom_signals_app/apps.py from django.apps import AppConfig class ApplicationConfig(AppConfig): name = "custom signal application" def codeReady(self): from application import receivers
You’ll need to add the config to your app’s init file once you’ve added the receiver. In this case, it is
default_app_config = 'custom_signals_app.apps.ApplicationConfig'
You don’t need to do this if you’re running Django 3.x+; instead, you can upgrade your installed app as shown below.
INSTALLED_APPS = ( ..., 'custom_signals_app.apps', ) # Just replace it with INSTALLED_APPS = ( ..., 'custom_signals_app.apps .apps.ApplicationConfig', )
That’s all there is to it. You’ve created a custom Django signal that works.
Positives of Django Signals
Let’s look at the benefits and applications of Django signals –
- The introduction of event-driven architecture in Django is Signal’s primary benefit. Even though Django signals are synchronous, it provides a new development paradigm called Decouple apps.
- If you’ve ever had to deal with a circular import problem, you know how tough it can be to resolve.
- Django signals are one of the solutions we’ve discovered.
- You can take multiple actions in response to a specific event. Use the Django signal whenever you have a lot of actions to perform on a single event.
- When a new user signs up, you might want to send them a welcome email, start a free trial, modify their experience, or help them effectively customize their profile.
Challenges with Django Signals
Let’s look at the downsides now:
- Greater responsibility comes with greater power. Signal debugging can be complex, especially if you’re new to Django. We’ve worked in a larger codebase where post_save signals were a nightmare, and we had to manually go through each file to figure out what went wrong a lot of the time.
- It’s important to know when to use send and send_robust. We send most of the time, which may cause concerns. Putting everything in a transaction may be an excellent solution. However, it may add to the DB load overhead.
- Django signals are something that most of us set up once for a project and use year after year, which is why remembering the tiny settings required to do the initial setup is so challenging.
Django comes with many features out of the box, and they’re all ready to use and battle-tested.
Though many developers utilize post_save signals and other model-related signals, very few new developers know that they can also develop custom signals. On the other hand, Django signals are one of the most underappreciated out-of-the-box features of Django.
One of the most powerful features allows you to decouple your app. Further, Signals act as event-driven architecture. Like any event-driven architecture, things may spiral out of control if you don’t have a proper guideline and design in place.