Django Signals mastery
Tags: django, restapi, backend, signals
Published: 2024-02-17 08:39:28

Django signals are a form of signal dispatching mechanisms that allow senders to notify a set of receivers when certain actions are executed in Django Framework. This is mostly common and used in decoupled applications where we need to establish a seamless communication between different components of the components effectively and efficiently. This allows developers to hook into specific moments of an application life cycle to respond to various actions or events such as the creation or update of a model instance. In Django we have mainly three types of signals namely [Model signals](https://docs.djangoproject.com/en/5.0/ref/signals/#module-django.db.models.signals), [Management signals](https://docs.djangoproject.com/en/5.0/ref/signals/#management-signals), [Request/Response signals](https://docs.djangoproject.com/en/5.0/ref/signals/#module-django.core.signals), [Test signals](https://docs.djangoproject.com/en/5.0/ref/signals/#module-django.test.signals), [Database wrappers](https://docs.djangoproject.com/en/5.0/ref/signals/#module-django.db.backends).

## Why Use Django Signals?

Django signals shine in scenarios requiring actions to be triggered by changes in your models. They facilitate a clean, decoupled architecture by allowing different parts of your application to communicate indirectly. Whether you're logging activity, sending notifications, or updating related objects upon changes, signals provide a robust, scalable way to implement these features without tightly coupling your components.

## How do Django signals work?
![Signals illustration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4x1e9h7thx6k66wqkp1r.png)

In a communication system, a transmitter encodes a message to create a signal, which is carried to a receiver by the communication channel. In Django we have a similar approach, at its core, the signal dispatching system enables certain senders (usually Django models) to notify asset of receivers (functions or methods) when certain events occur. For instance, you might want to automatically send a welcome email to a user immediately after their account has been created. With Django signals, this process is streamlined: a signal is dispatched when a new user is saved, and a receiver function listening for this signal triggers the email sending process.

## Working with built-in signals
Django's built-in signals are powerful tools that allow developers to hook into specific framework operations, such as model saving, deleting, and request processing. Understanding how to effectively work with these signals can significantly enhance the functionality and efficiency of your Django applications. Let's dive into this more and study the **most used signals**
### Pre-init and post-init signals
The signals `pre_init` and `post_init` signals provide powerful hooks into the Django model lifecycle, allowing for sophisticated initialization logic and runtime attribute management. When used judiciously, they can enhance the flexibility and capabilities of your Django models without significantly impacting performance or maintainability.

#### Use Cases
Usage of this signal includes several scenarios. Initializing non-database attributes, logging or monitoring instance creation, conditionally modifying attributes.

##### Implementation Guide
To connect to these signals, you use the receiver decorator or explicitly call the connect method on the signal. Here is an example of using the post_init signal to set up a custom attribute on a model instance:
```python
from django.db.models.signals import post_init
from django.dispatch import receiver
from myapp.models import MyModel

@receiver(post_init, sender=MyModel)
def setup_custom_attributes(sender, instance, **kwargs):
    if not hasattr(instance, 'custom_attribute'):
        instance.custom_attribute = 'default value'
```
This example checks if the custom_attribute is already set on the instance and sets a default value if it's not. This could be useful for instances loaded from the database as well as newly created ones.
### Pre-save and Post-save Signals
The `pre_save` and `post_save` signals are dispatched before and after Django's ORM calls the `save` method on a model instance, respectively. These signals are incredibly useful for executing logic either before or after a model instance is committed to the database.

#### Use Cases
Some of the use cases include: Auto-timestamping(automatically update `last_modified` fields on models before they are saved, Data Validation/Normalization, Cache Invalidation.

#### Implementation Guide
Connecting to these signals requires defining a receiver function and using the `@receiver` decorator or the `signal.connect()` method. Here's an example of using `post_save` to update a user profile every time a user instance is saved.

```python
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from myapp.models import UserProfile

@receiver(post_save, sender=User)
def update_user_profile(sender, instance, created, **kwargs):
    UserProfile.objects.update_or_create(user=instance)
```
### Pre-delete and Post-delete Signals
Similar to save signals, `pre_delete `and `post_delete` are dispatched before and after a model instance is deleted. These are ideal for performing cleanup actions or logging. When using these signals with models that are often deleted in bulk, remember that signals fire for each instance, which might impact performance. It's also important to handle exceptions gracefully within receivers.

#### Use Cases
Logging Deletions, cleaning up of related files or orphan database records that are no longer needed.

#### Implementation Guide
Check the implementation below for more understanding.
```python
from django.db.models.signals import post_delete
from django.dispatch import receiver
from myapp.models import MyModel

@receiver(post_delete, sender=MyModel)
def log_deleted_instance(sender, instance, **kwargs):
    logger.info(f'MyModel instance {instance.id} deleted.')
```
### Request/Response Signals
Django includes signals such as request_started, request_finished, and got_request_exception that are tied to the lifecycle of an HTTP request. These can be used for monitoring, debugging, or modifying request/response behavior. While useful, attaching too much logic to these signals can affect the overall performance of your application. Keep the code within receivers lightweight and consider asynchronous operations for heavier tasks.

#### Use Cases
Exception handling by performing actions or notification when uncaught exception occurs during a request.

#### Implementation Guide
To monitor request durations, you might connect to both `request_started` and `request_finished` signals:

```python
from django.core.signals import request_started, request_finished

@receiver(request_started)
def start_request_timer(sender, **kwargs):
    request.start_time = time.time()

@receiver(request_finished)
def calculate_request_duration(sender, **kwargs):
    duration = time.time() - request.start_time
    logger.info(f'Request took {duration} seconds.')
```
### m2m_changed Signal
The `m2m_changed` signal is dispatched when a `ManyToManyField` on a model is changed. It can occur before or after the change (addition, removal, clear) is applied.

#### Use cases
Tracking changes in many-to-many relationships, such as updating cache or validating changes to a set of related objects.

#### Implementation Guide:
This example demonstrates how to listen for changes to a ManyToManyField on MyModel and perform actions based on the type of change.
```python
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from myapp.models import MyModel

@receiver(m2m_changed, sender=MyModel.my_field.through)
def m2m_changed_handler(sender, instance, action, reverse, model, pk_set, **kwargs):
    if action == "post_add":
        # Handle post-addition of related objects
        pass
```

### class_prepared Signal
Fired once the model class is fully defined and ready, but before any instances are created. It allows for modifying model attributes or dynamically adding fields.

#### Use cases
Dynamically adding fields or methods to models based on certain conditions or external configurations.

#### Implementation Guide
```python
from django.db.models.signals import class_prepared
from django.dispatch import receiver

@receiver(class_prepared)
def enhance_model(sender, **kwargs):
    if sender.__name__ == 'MySpecialModel':
        # Dynamically add a field or method to MySpecialModel
        pass
```
### pre_migrate and post_migrate Signals
These signals are dispatched before and after Django runs migrations. They are useful for setting up or tearing down resources related to migrations.

#### Implementation Guide
The implementation below can be used to ensure data consistency or to perform custom schema updates that are outside the scope of standard migrations.

```python
from django.db.models.signals import post_migrate
from django.dispatch import receiver

@receiver(post_migrate)
def perform_post_migration_actions(sender, **kwargs):
    # Perform any necessary actions after migrations have completed
    pass
```

## Summarizer 😜
| Signal Name       | Description                                                                 | Use Cases                                                                                   | Implementation Note                                                                                   |
|-------------------|-----------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|
| `pre_save`        | Dispatched before a model’s `save()` method is called.                     | Auto-timestamping, data validation/normalization, cache invalidation.                      | Connect signal to a receiver function that performs necessary actions before a model instance is saved. |
| `post_save`       | Dispatched after a model’s `save()` method is called.                      | Sending signals, post-save operations like sending emails or processing data.               | Implement a receiver that acts on model instance after it's been saved to the database.                |
| `pre_delete`      | Dispatched before a model’s `delete()` method or queryset’s `delete()` is called. | Cleaning up related objects, logging deletions.                                            | Connect to a receiver that handles cleanup or logging before an instance is deleted.                   |
| `post_delete`     | Dispatched after a model’s `delete()` method or queryset’s `delete()` is called.  | Post-delete actions, such as cache invalidation or updating related data.                  | Use receivers to perform actions after an instance is deleted.                                         |
| `m2m_changed`     | Dispatched when a `ManyToManyField` on a model is changed.                 | Tracking changes in many-to-many relationships, updating caches, validating changes.       | Attach a receiver to handle the specific actions (add, remove, clear) for m2m changes.                 |
| `pre_init`        | Dispatched before a model’s `__init__` method is called.                   | Setting up non-database attributes, modifying initialization parameters.                   | Define a receiver to modify attributes or perform actions before model instance initialization.        |
| `post_init`       | Dispatched after a model’s `__init__` method is called.                    | Additional setup based on the initialized model instance.                                  | Implement a receiver to perform setup actions that depend on the initialized state of the instance.    |
| `request_started` | Dispatched at the start of each HTTP request.                              | Request logging/monitoring, setting up request-specific resources.                         | Connect a receiver function to perform actions at the beginning of a request.                         |
| `request_finished`| Dispatched at the end of each HTTP request.                                | Cleanup actions post-request, logging request metrics.                                     | Use a receiver to clean up resources or log information after a request has been processed.            |
| `class_prepared`  | Dispatched once a model class is fully prepared.                           | Dynamically adding fields/methods to models, adjusting model attributes.                   | Attach a receiver to modify or enhance model classes dynamically.                                      |
| `pre_migrate`     | Dispatched before running migrations.                                     | Preparing data or schema changes not covered by migrations, setup actions before migration. | Implement actions to be performed before migrations are applied to the database.                       |
| `post_migrate`    | Dispatched after running migrations.                                       | Data consistency checks, performing custom schema updates.                                 | Use receivers to ensure data integrity or make additional changes after migrations.                    |


> ⚠️ Implementation of signals can significantly affect the performance and speed of your application both positively and negatively. Consider keeping the code in the signals and light and switch to tasks for heavier processes.

## Conclusion
Most signals used in Django were used in this post. Always good and advised to reference yourself on the [Django project](https://docs.djangoproject.com/en/5.0/ref/signals/) website and remember relying heavily on signals can make application logic harder to follow, especially for new developers or when debugging.