Django signals let you hook into the Django ORM and other parts of the framework, allowing code to run automatically when certain events happen.

Let’s see a signal in action. Imagine you want to automatically create a user profile whenever a new User is created.

# users/models.py
from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True)

    def __str__(self):
        return f'{self.user.username} Profile'

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

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

Here, post_save is a signal that fires after a model instance is saved. The @receiver decorator connects our create_user_profile and save_user_profile functions to this signal, specifically when a User model is saved. The create argument in the create_user_profile function tells us if a new User object was just created, and if so, we create a corresponding Profile. The save_user_profile ensures that any changes to the User also trigger a save on their associated Profile.

Signals solve the problem of wanting to perform side effects without tightly coupling your code. Instead of modifying your User model’s save method to also create a Profile, you can keep the concerns separate. The User model doesn’t need to know about Profiles; the signal handler takes care of that. This promotes a cleaner, more modular design, especially in larger projects where multiple applications might need to react to the same events. You can define signals for model creation, deletion, updates, request handling, and more.

The mental model for signals is an event-driven architecture within Django. Think of it like a broadcast system: when an event occurs (e.g., a User is saved), Django broadcasts a signal. Any listening functions (receivers) that have registered for that specific signal and sender will then execute. You can send custom signals too, allowing your own applications to communicate in this decoupled way.

The real power comes from the flexibility. You can connect multiple receivers to a single signal, or have a single receiver listen to multiple signals. You can also disconnect receivers dynamically using signal.disconnect(). This makes them incredibly powerful for extending Django’s behavior without modifying its core or even other applications’ code directly. For instance, a third-party app could emit a signal, and your app could hook into it to perform custom actions, like sending an email or updating a separate analytics system.

One thing that often trips people up is the implicit nature of signals. Because they run automatically, it can be hard to trace the flow of execution, especially when multiple signals and receivers are involved. Debugging can become a challenge if you’re not careful about what signals are being sent and by whom. If a signal handler raises an exception, it can sometimes be caught by Django’s default error handling or propagate in unexpected ways, making it difficult to pinpoint the root cause. This is why understanding the signal flow and having good logging is crucial.

The next logical step is exploring how to manage signal order and prevent infinite loops when signals trigger other signals.

Want structured learning?

Take the full Django course →