Skip to main content

Django Built-in Signals

Django's signal framework provides a way for applications to get notified when certain events occur elsewhere in the framework. Django ships with a set of built-in signals that allow your code to respond to various events in the framework's lifecycle.

What are Built-in Signals?

Built-in signals are pre-defined signals that Django automatically sends at various points during the execution of its code. These signals allow your application to perform actions in response to specific events without modifying Django's core functionality.

Think of them as notifications that Django sends out saying, "Hey, I just performed this action, in case you're interested."

Why Use Built-in Signals?

  • Loose coupling: Your code can react to events without being tightly integrated with the code that triggers those events
  • Cross-cutting concerns: Handle functionality that spans multiple apps (like logging, caching)
  • Extensibility: Add functionality to built-in Django features without subclassing

Common Built-in Signals

Model Signals

The most commonly used signals in Django relate to model operations. Let's explore them with examples:

pre_save and post_save

These signals are sent before and after a model's save() method is called.

python
from django.db.models.signals import pre_save, post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from .models import Profile

# This signal creates a user profile when a new user is created
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
print(f"Profile created for user: {instance.username}")

# This signal saves the profile whenever the user is saved
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
instance.profile.save()
print(f"Profile saved for user: {instance.username}")

The post_save signal receives several arguments:

  • sender: The model class that sent the signal (User in this case)
  • instance: The actual instance being saved
  • created: Boolean indicating if this is a new instance
  • **kwargs: Additional keywords arguments

pre_delete and post_delete

These signals are sent before and after a model's delete() method is called.

python
from django.db.models.signals import post_delete
from django.dispatch import receiver
from .models import Profile

@receiver(post_delete, sender=Profile)
def delete_user_files(sender, instance, **kwargs):
# Delete the user's profile picture when profile is deleted
if instance.profile_picture:
instance.profile_picture.delete(False)
print(f"Deleted profile picture for {instance.user.username}")

m2m_changed

This signal is sent when a ManyToMany relationship is changed.

python
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from .models import Article, Tag

@receiver(m2m_changed, sender=Article.tags.through)
def tags_changed(sender, instance, action, pk_set, **kwargs):
if action == 'post_add':
print(f"Tags {list(pk_set)} were added to article: {instance.title}")
elif action == 'post_remove':
print(f"Tags {list(pk_set)} were removed from article: {instance.title}")

Request/Response Signals

Django also provides signals related to HTTP request processing:

request_started and request_finished

python
from django.core.signals import request_started, request_finished
from django.dispatch import receiver
import time

# Store request start time as a global variable
request_start_time = {}

@receiver(request_started)
def track_request_start(sender, environ, **kwargs):
request_id = id(environ)
request_start_time[request_id] = time.time()
print(f"Request started: {environ['PATH_INFO']}")

@receiver(request_finished)
def track_request_end(sender, **kwargs):
# In a real application, you'd need to properly track the request ID
# This is simplified for demonstration purposes
print(f"Request finished")

got_request_exception

python
from django.core.signals import got_request_exception
from django.dispatch import receiver
import logging

logger = logging.getLogger(__name__)

@receiver(got_request_exception)
def log_exception(sender, request, **kwargs):
logger.error(f"Exception occurred during request {request.path}",
exc_info=True)

Management Signals

These signals are sent during Django management command operations:

python
from django.core.signals import pre_migrate, post_migrate
from django.dispatch import receiver

@receiver(pre_migrate)
def before_migrate(sender, app_config, verbosity, interactive, **kwargs):
if app_config:
print(f"About to migrate {app_config.label}...")
else:
print(f"About to migrate all apps...")

@receiver(post_migrate)
def after_migrate(sender, app_config, verbosity, interactive, **kwargs):
if app_config:
print(f"Finished migrating {app_config.label}")
else:
print(f"Finished migrating all apps")

User Signals

Django's authentication framework has its own set of signals:

python
from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed
from django.dispatch import receiver

@receiver(user_logged_in)
def on_user_login(sender, request, user, **kwargs):
print(f"User {user.username} logged in")

# Update last login IP
ip = request.META.get('REMOTE_ADDR')
user.profile.last_login_ip = ip
user.profile.save()

@receiver(user_logged_out)
def on_user_logout(sender, request, user, **kwargs):
if user:
print(f"User {user.username} logged out")

@receiver(user_login_failed)
def on_login_failed(sender, credentials, request, **kwargs):
print(f"Login failed for username: {credentials.get('username', 'unknown')}")

Real-world Applications

Let's look at some practical examples of how built-in signals can be used to solve real problems:

Example 1: Sending Welcome Email

python
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from django.dispatch import receiver
from django.core.mail import send_mail
from django.conf import settings

@receiver(post_save, sender=User)
def send_welcome_email(sender, instance, created, **kwargs):
if created:
subject = "Welcome to Our Site!"
message = f"Hi {instance.username},\n\nThank you for registering on our site!"

try:
send_mail(
subject,
message,
settings.DEFAULT_FROM_EMAIL,
[instance.email],
fail_silently=False,
)
print(f"Welcome email sent to {instance.email}")
except Exception as e:
print(f"Failed to send welcome email: {str(e)}")

Example 2: Generating Slugs

python
from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.utils.text import slugify
from .models import Article

@receiver(pre_save, sender=Article)
def create_article_slug(sender, instance, **kwargs):
# Only generate slug when it's a new article or if title has changed
if not instance.slug or not Article.objects.filter(pk=instance.pk).exists() or \
Article.objects.get(pk=instance.pk).title != instance.title:

base_slug = slugify(instance.title)
slug = base_slug
counter = 1

# Check if slug exists and increment counter if needed
while Article.objects.filter(slug=slug).exists():
slug = f"{base_slug}-{counter}"
counter += 1

instance.slug = slug
print(f"Generated slug: {slug}")

Example 3: Audit Logging

python
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.contrib.contenttypes.models import ContentType
from .models import AuditLog, Product

@receiver([post_save, post_delete], sender=Product)
def log_product_changes(sender, instance, **kwargs):
action = 'created' if kwargs.get('created', False) else ('deleted' if 'created' not in kwargs else 'updated')

AuditLog.objects.create(
content_type=ContentType.objects.get_for_model(sender),
object_id=instance.id,
action=action,
object_repr=str(instance),
user_id=getattr(instance, '_user_id', None) # Assuming you set _user_id somewhere in your code
)
print(f"Audit log created for {action} action on {instance}")

Connecting to Built-in Signals

There are two main ways to connect to Django's built-in signals:

This is the most common and readable approach:

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

@receiver(post_save, sender=User)
def user_saved_handler(sender, instance, created, **kwargs):
print(f"User saved: {instance.username}")

2. Using the connect() Method

An alternative approach, useful when you need to connect signals dynamically:

python
from django.db.models.signals import post_save
from django.contrib.auth.models import User

def user_saved_handler(sender, instance, created, **kwargs):
print(f"User saved: {instance.username}")

post_save.connect(user_saved_handler, sender=User)

Best Practices for Using Built-in Signals

  1. Place signal handlers in a dedicated file: Create a signals.py file in your app and import it in your app's __init__.py or apps.py
python
# In your app's signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from .models import Profile

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
python
# In your app's apps.py
from django.apps import AppConfig

class MyAppConfig(AppConfig):
name = 'myapp'

def ready(self):
import myapp.signals # This imports your signals
  1. Keep signal handlers lightweight: Avoid complex processing in signal handlers that could slow down the main operation

  2. Handle exceptions properly: Signal handlers should not crash, as they could interrupt the main flow of the application

  3. Be aware of signal ordering: Multiple handlers for the same signal are called in undefined order, so don't rely on specific ordering

  4. Disconnect signals when needed: Particularly in tests to avoid side effects

python
from django.db.models.signals import post_save
from myapp.signals import create_user_profile

# Temporarily disconnect a signal
post_save.disconnect(create_user_profile, sender=User)

# Do something without the signal being triggered

# Reconnect the signal later
post_save.connect(create_user_profile, sender=User)

Common Issues and Solutions

Signals Firing Multiple Times

Problem: Signal handlers being executed multiple times for a single event.

Solution:

  • Check if the signal is being registered multiple times
  • Make sure you're not importing the signals module in multiple places

Circular Import Issues

Problem: Importing signal handlers can sometimes cause circular imports.

Solution:

  • Use lazy loading by importing inside functions
  • Use Django's AppConfig.ready() method to register signals

Testing with Signals

Problem: Signals can cause side effects in tests.

Solution:

  • Disconnect signals in test setup
  • Use the @override_settings decorator to disable specific signals
  • Create fixtures that account for signal-created objects
python
from django.test import TestCase, override_settings
from django.db.models.signals import post_save
from myapp.signals import create_user_profile

class UserTest(TestCase):
def setUp(self):
# Disconnect signals for this test
post_save.disconnect(create_user_profile, sender=User)

def tearDown(self):
# Reconnect signals after tests
post_save.connect(create_user_profile, sender=User)

Summary

Django's built-in signals provide a powerful way to respond to events in the framework without modifying its core code. They are particularly useful for:

  • Creating related objects automatically
  • Performing actions before or after model operations
  • Logging and auditing
  • Handling cross-cutting concerns

While signals offer many advantages, they should be used judiciously as they can sometimes make code flow harder to follow. When used appropriately, however, they help keep your Django applications modular and maintainable.

Additional Resources

Exercises

  1. Create a signal handler that logs to a file whenever a model is created, updated, or deleted.
  2. Implement a feature that sends a notification email to admins whenever a user changes their password.
  3. Create a signal handler that automatically creates thumbnails whenever an image is uploaded.
  4. Use signals to implement a simple cache invalidation system that clears cached data when models are updated.
  5. Build a system that tracks and logs all database changes made during a request using the request signals.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)