Model or dataset
rsinger86/django-lifecycle avatar
rsinger86/django-lifecycle

django-lifecycle: Declarative Hooks for Django Models

Declarative model lifecycle hooks, an alternative to Signals.

1,422 stars97 forksPythonMIT

At a glance

What is it?
django-lifecycle places conditional model callbacks on the model class through a `@hook` decorator, a base model, or a mixin. It suits Django developers who want lifecycle behavior beside model fields while avoiding the indirection of Signals.
Who is it for?
Adopt django-lifecycle when model-specific behavior is naturally owned by the model class and you want explicit conditions instead of dispersed Signal receivers. Do not choose it merely to replace Signals globally, especially when cross-model coordination is the main requirement.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 147 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 6, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • rsinger86/django-lifecycle1,422 ★ · +17
Star history
Repository2026-10-172026-11-06
rsinger86/django-lifecycle1.4k1.4k

17 Stars in the last 20 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

Why django-lifecycle Moves Model Logic Out of Signals

django-lifecycle addresses a specific code-organization problem in Django applications: behavior attached to a model's own lifecycle can become scattered across Signal receivers or custom `save` and `__init__` methods. The README describes Signals as introducing unnecessary indirection and conflicting with Django's "fat models" approach. Its alternative is to declare lifecycle methods on the model that owns the fields involved.

The intended audience is Python and Django developers whose business rules include actions such as setting an update timestamp, recording an original field value, or notifying an editor after a status transition. The package supports Python 3.8 through 3.14 and Django 4.2, 5.0, 5.2, and 6.0 according to the README and `setup.py` classifiers.

This is not a general workflow engine. The visible API is centered on model operations and field conditions. If a requirement involves coordinating several unrelated models, Django Signals may remain more direct because receivers can be connected outside the model that defines the lifecycle rule.

The `@hook` Decorator and Django Lifecycle Stages

The central mechanism is a `@hook` decorator imported from `django_lifecycle`. A decorated method names a lifecycle stage and can receive a condition. The README's example uses `BEFORE_UPDATE` when `contents` has changed and `AFTER_UPDATE` when `status` moves from `draft` to `published`.

The before-update hook modifies the model before persistence is expected to occur:

python
from django_lifecycle import LifecycleModel, hook, BEFORE_UPDATE
from django_lifecycle.conditions import WhenFieldHasChanged

@hook(BEFORE_UPDATE, WhenFieldHasChanged("contents", has_changed=True))
def on_content_change(self):
    self.updated_at = timezone.now()

In the complete README model, that method belongs to an `Article` inheriting from `LifecycleModel`. The package also offers a base model and a mixin, but the README does not show the mixin form, so its exact declaration syntax is not documented there.

Field Conditions for Change Detection and Transitions

Conditions are defined in `django_lifecycle.conditions` and use declarative field comparisons. Three named conditions appear in the README: `WhenFieldValueIs`, `WhenFieldValueWas`, and `WhenFieldHasChanged`. The first two are useful when the hook must distinguish the current value from the value retained by the lifecycle mechanism. The third expresses a direct changed-field predicate through its `has_changed=True` argument.

The publishing example composes conditions with `&` and wraps the expression in `condition=`. It triggers only when the old status was `draft` and the current status is `published`, rather than whenever the current value merely equals `published`:

python
@hook(
    AFTER_UPDATE,
    condition=(
        WhenFieldValueWas("status", value="draft")
        & WhenFieldValueIs("status", value="published")
    )
)
def on_publish(self):
    send_email(self.editor.email, "An article has published!")

This explicit transition condition prevents a hook intended for one state change from firing on unrelated updates while the status remains `published`. The README does not enumerate every available condition operator or document behavior for multiple changes before persistence.

A First Article Model with Update and Publish Hooks

The README gives this as its main example. The surrounding imports include Django's `models` and `timezone`, which are omitted from the isolated snippet below. In a Django application, `Article` would use `LifecycleModel`, declare its fields, and retain both hook methods on that class.

python
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, WhenFieldHasChanged("contents", has_changed=True))
    def on_content_change(self):
        self.updated_at = timezone.now()

The second hook remains on the same model and checks the old and new status values before sending an email. The README contrasts this with manual overrides that save original `contents` and `status` attributes in `__init__`, compare them inside `save`, and perform post-save notification logic. The declarative version keeps those rules visible as named methods with their conditions beside the affected fields.

The README does not show the application's `models.py` path, migrations, URL configuration, or the context in which `send_email()` is called. It therefore does not provide enough information for a complete runnable application example.

Installation Documentation and the Repository Test Suite

The README does not give an explicit consumer installation command such as a `pip install` example. It links to the project's PyPI package and documentation, while `setup.py` identifies the distribution as `django-lifecycle` and declares the package modules. For installation, the documented destination is therefore the PyPI page and the project documentation rather than an install command reproduced here.

The only concrete execution instructions are for the repository's test environment. The README says to install the project requirements and then run `./manage.py test`. It describes the tests as living in a simplified Django project under `/tests`.

bash
./manage.py test

The repository also contains `manage.py`, `requirements.txt`, `tox.ini`, and `tests/`, which is consistent with the documented test layout. `requirements.txt` pins `Django==5.2` and includes documentation and development dependencies, so it should not be treated as a minimal installation manifest for application authors. The README does not document a separate production dependency list.

Where Model Hooks Are the Wrong Boundary

The principal limitation is organizational rather than a documented defect. django-lifecycle places behavior directly in the model class. That is useful when a rule is owned by one model, as the article example demonstrates, but it can be the wrong boundary for behavior that belongs to several models or to application-level coordination. A Signal can provide that external connection point, while the package's model-centric design deliberately removes that separation.

The examples also perform side effects inside lifecycle methods. The post-update publishing hook calls `send_email()`, but the documentation does not describe transaction boundaries, retries, idempotency, exception handling, or what happens when sending fails. An application that requires precise control over those cases would need to establish that behavior separately or inspect the full documentation and tests.

The package does not claim to replace every use of Django Signals. It offers a narrower choice for rules that naturally belong beside a model's fields. The README also does not document bulk operations, queryset-level changes, or transaction callbacks, so adoption should not assume that a hook will fire for every possible database access pattern.

Signals, Manual Overrides, and django-lifecycle's Actual Difference

Django Signals are the main alternative identified by the README. Signals connect receivers to named events and can be registered outside the originating model. django-lifecycle instead uses inherited or mixed-in model behavior plus decorated methods. The key difference is where the rule is declared: the README favors a colocated model method over an externally connected receiver.

Manual `save` and `__init__` overrides form the second comparison. Those overrides can implement any custom behavior, but the example shows how field snapshots and conditional logic can spread through framework methods. Decorators make each rule and condition separately visible, which helps when a model has several field-driven actions.

There is a trade-off. Signals can centralize cross-cutting handlers and avoid adding methods to a model. Lifecycle hooks are clearer when the model owns the invariant. Neither approach should be selected solely by syntax, because the package documentation does not establish that lifecycle hooks offer better transaction safety, delivery guarantees, or performance than Signals.

Release History, Compatibility, and the MIT License

The repository is not archived, and its last push was on 2026-06-14. Recent releases include 1.3.0 on 2026-06-14, 1.2.7 on 2026-03-02, and 1.2.6 on 2026-01-25. That history records recent project activity at those points, but the README does not summarize the individual changes in those versions. It directs readers to `CHANGELOG.md`; its contents are not shown here, so migration steps and compatibility changes cannot be stated safely.

`setup.py` classifies the project as `Development Status :: 4 - Beta` and lists supported Python and Django versions. It also identifies the distribution under the `MIT License`. The repository contains a separate `LICENSE.md`, but its full terms are not included. The MIT label permits reuse under that license's conditions, subject to retaining the required license notice, but this article does not provide legal advice.

An upgrade review should focus first on `CHANGELOG.md` and the installed Django version. Since the README does not document a complete consumer upgrade procedure, the package should not be upgraded solely from its version number without checking that changelog.

Editorial conclusion

Adopt django-lifecycle when model-specific behavior is naturally owned by the model class and you want explicit conditions instead of dispersed Signal receivers. Do not choose it merely to replace Signals globally, especially when cross-model coordination is the main requirement. Before adoption, verify hook timing against your workflow, inspect `CHANGELOG.md` for upgrade changes affecting your Django version, and confirm that placing side effects inside model operations matches your transaction and failure-handling requirements.

Frequently asked questions

How do I add a lifecycle hook to a Django model with django-lifecycle?

Inherit from `LifecycleModel` or use the package's mixin, then decorate model methods with `@hook`. Pass a lifecycle stage such as `BEFORE_UPDATE` or `AFTER_UPDATE`, with a condition when the hook should run selectively.

How do I run a hook only when a field changes in django-lifecycle?

Use `WhenFieldHasChanged` with the field name and `has_changed=True`. The README applies it to `contents` in a `BEFORE_UPDATE` hook.

How do I detect a field transition in django-lifecycle?

Combine `WhenFieldValueWas` and `WhenFieldValueIs` with `&`. The README uses this pattern to run an `AFTER_UPDATE` hook when `status` changes from `draft` to `published`.

Can django-lifecycle run email and other side effects from model hooks?

The README demonstrates calling `send_email()` from an `AFTER_UPDATE` hook. It does not document transaction behavior, retries, or failure handling for that operation.

Which Django versions are listed for django-lifecycle?

The README and `setup.py` list Django 4.2, 5.0, 5.2, and 6.0. `requirements.txt` currently includes `Django==5.2`, but that file also contains development and documentation dependencies.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. rsinger86/django-lifecycle on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/rsinger86-django-lifecycle.svg)](https://hysenlabs.com/projects/rsinger86-django-lifecycle)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/rsinger86-django-lifecycle.svg)](https://hysenlabs.com/projects/rsinger86-django-lifecycle)

Was this useful?