The post_save Signal
In this page:
What Triggers post_save?
post_save fires right after save() finishes writing to the database. Django passes it sender (the model class), instance (the saved object), and created (True for a new row).
Example: What Triggers post_save?
post_save fires right after save() finishes writing to the database. Django passes it sender (the model class), instance (the saved object), and created (True for a new row).
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Profile
@receiver(post_save, sender=Profile)
def profile_saved(sender, instance, created, **kwargs):
print(f"Profile {instance.pk} saved, created={created}")
{# Django-only code -- models.py/views.py/urls.py/settings.py
snippets, or template markup using Django template tags/variables
-- can't run standalone via Judge0 or the browser preview, since
it needs a real Django project. Only this course's pure-Python
examples (example_lang == 'python', no Django imports) are
actually runnable, so those still get the button below. #}
Using the created Flag
A very common pattern is to run setup logic only the first time a row is inserted, such as auto-creating a related Profile whenever a new User is created.
Example: Using the created Flag
A very common pattern is to run setup logic only the first time a row is inserted, such as auto-creating a related Profile whenever a new User is created.
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_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
{# Django-only code -- models.py/views.py/urls.py/settings.py
snippets, or template markup using Django template tags/variables
-- can't run standalone via Judge0 or the browser preview, since
it needs a real Django project. Only this course's pure-Python
examples (example_lang == 'python', no Django imports) are
actually runnable, so those still get the button below. #}
Avoiding Infinite Save Loops
Calling instance.save() again inside a post_save receiver for the same model re-triggers post_save, which can loop forever. Use QuerySet.update() instead, since it doesn't re-fire the signal.
Warning: Never call instance.save() unguarded inside its own post_save receiver.
Example: Avoiding Infinite Save Loops
Calling instance.save() again inside a post_save receiver for the same model re-triggers post_save, which can loop forever. Use QuerySet.update() instead, since it doesn't re-fire the signal.
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Article
@receiver(post_save, sender=Article)
def set_slug(sender, instance, created, **kwargs):
if created and not instance.slug:
Article.objects.filter(pk=instance.pk).update(slug=f"article-{instance.pk}")
{# Django-only code -- models.py/views.py/urls.py/settings.py
snippets, or template markup using Django template tags/variables
-- can't run standalone via Judge0 or the browser preview, since
it needs a real Django project. Only this course's pure-Python
examples (example_lang == 'python', no Django imports) are
actually runnable, so those still get the button below. #}
Scoping with sender
Passing sender= when connecting limits a receiver to one model. The same receiver function can be connected to multiple senders if you want shared behavior.
Example: Scoping with sender
Passing sender= when connecting limits a receiver to one model. The same receiver function can be connected to multiple senders if you want shared behavior.
from django.db.models.signals import post_save
from .models import Article, Profile
def log_change(sender, instance, **kwargs):
print(f"{sender.__name__} #{instance.pk} changed")
post_save.connect(log_change, sender=Article)
post_save.connect(log_change, sender=Profile)
{# Django-only code -- models.py/views.py/urls.py/settings.py
snippets, or template markup using Django template tags/variables
-- can't run standalone via Judge0 or the browser preview, since
it needs a real Django project. Only this course's pure-Python
examples (example_lang == 'python', no Django imports) are
actually runnable, so those still get the button below. #}
- Forgetting to check the created flag and running 'new object' logic on every update too.
- Causing infinite loops by calling instance.save() again inside a post_save receiver on the same model without a guard.
- Connecting a receiver but never importing the module that contains it, so it silently never runs.
- post_save fires immediately after a model instance's save() method completes.
- The created boolean tells you whether this was an INSERT or an UPDATE.
- Use sender=Model to scope a receiver to one specific model.
- Avoid saving the same instance again inside its own post_save receiver to prevent recursion.
Chapter Quiz — Complete all 8 topics to unlock
0/8 topics done
Complete these topics first: