Declarative model lifecycle hooks, an alternative to Signals.

0 2 281 8
  • Created: March 18, 2018
  • Last Release: Oct. 8, 2019
  • Last Commit: Oct. 8, 2019

Django Lifecycle Hooks

Package version Python versions

This project provides a @hook decorator as well as a base model and mixin to add lifecycle hooks to your Django models. Django's built-in approach to offering lifecycle hooks is Signals . However, my team often finds that Signals introduce unnesseary indirection and are at odds with Django's "fat models" approach.

In short, you can write model code like this:

from django_lifecycle import LifecycleModel, hook

class Article(LifecycleModel):
    contents = models.TextField()
    updated_at = models.DateTimeField(null=True)
    status = models.ChoiceField(choices=['draft', 'published'])
    editor = models.ForeignKey(AuthUser)

    @hook('before_update', when='contents', has_changed=True)
    def on_content_change(self):
        self.updated_at =

    @hook('after_update', when="status", was="draft", is_now="published")
    def on_publish(self):
        send_email(, "An article has published!")

Instead of overriding save and __init___ in a clunky way that hurts readability:

    # same class and field declarations as above ...
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._orig_contents = self.contents
        self._orig_status = self.status
    def save(self, *args, **kwargs):
        if is not None and self.contents != self._orig_contents):
            self.updated_at =

        super().save(*args, **kwargs)

        if self.status != self._orig_status:
            send_email(, "An article has published!")

Documentation :

Source Code :


0.6.0 (October 2019)

  • Adds when_any hook paramter to watch multiple fields for state changes

0.5.0 (September 2019)

  • Adds was_not condition
  • Allow watching changes to FK model field values, not just FK references

0.4.2 (July 2019)

  • Fixes missing issue that broke install.

0.4.1 (June 2019)

0.4.0 (May 2019)

  • Fixes initial_value(field_name) behavior - should return value even if no change. Thanks @adamJLev!

0.3.2 (February 2019)

  • Fixes bug preventing hooks from firing for custom PKs. Thanks @atugushev!

0.3.1 (August 2018)

  • Fixes m2m field bug, in which accessing auto-generated reverse field in before_create causes exception b/c PK does not exist yet. Thanks @garyd203!

0.3.0 (April 2018)

  • Resets model's comparison state for hook conditions after save called.

0.2.4 (April 2018)

  • Fixed support for adding multiple @hook decorators to same method.

0.2.3 (April 2018)

  • Removes residual mixin methods from earlier implementation.

0.2.2 (April 2018)

  • Save method now accepts skip_hooks , an optional boolean keyword argument that controls whether hooked methods are called.

0.2.1 (April 2018)

  • Fixed bug in _potentially_hooked_methods that caused unwanted side effects by accessing model instance methods decorated with @cache_property or @property .

0.2.0 (April 2018)

  • Added Django 1.8 support. Thanks @jtiai!
  • Tox testing added for Python 3.4, 3.5, 3.6 and Django 1.8, 1.11 and 2.0. Thanks @jtiai!


Tests are found in a simplified Django project in the /tests folder. Install the project requirements and do ./ test to run them.


See License .