Django Lifecycle
Django Lifecycle is a Python library that provides declarative lifecycle hooks for Django models. It allows developers to define methods that run automatically before or after database operations (e.g., save, delete, create, update) or when specific model field values change, using simple decorators. The current version is 1.2.7, and it maintains an active release cadence with frequent bug fixes and feature enhancements.
Common errors
-
AttributeError: 'MyModel' object has no attribute 'initial_value'
cause Your Django model is not inheriting from `LifecycleModelMixin`, or `LifecycleModelMixin` is not the first base class in the inheritance chain.fixEnsure your model class inherits `LifecycleModelMixin` as the first parent: `class MyModel(LifecycleModelMixin, models.Model):`. -
ImportError: cannot import name 'condition' from 'django_lifecycle'
cause You are trying to import the `condition` decorator or other predefined conditions (e.g., `is_greater_than`) directly from the top-level `django_lifecycle` package.fixImport conditions from the `django_lifecycle.conditions` submodule: `from django_lifecycle.conditions import condition` or `from django_lifecycle.conditions import is_greater_than`. -
TypeError: Lifecycle hooks require a method decorated with @hook.
cause A method in your `LifecycleModelMixin` inherited model is intended to be a hook but is missing the `@hook(...)` decorator, or the decorator is improperly applied.fixVerify that all methods intended to be lifecycle hooks are correctly decorated with `@hook` and specify the event type, e.g., `@hook(BEFORE_SAVE)`. -
ValueError: Invalid hook event: 'MY_CUSTOM_EVENT'
cause You've used an event name in the `@hook` decorator that is not one of the predefined `django-lifecycle` events (e.g., `BEFORE_SAVE`, `AFTER_CREATE`).fixUse one of the predefined event constants from `django_lifecycle` (e.g., `BEFORE_SAVE`, `AFTER_UPDATE`, `BEFORE_DELETE`, etc.). If you need custom logic, use `when` conditions or `condition` decorators on existing events.
Warnings
- breaking `django-lifecycle` versions 1.2.7 and above have removed support for Django versions prior to 4.2. Upgrading `django-lifecycle` to 1.2.7+ in projects running older Django versions will lead to `ImportError` or other runtime issues.
- gotcha The `LifecycleModelMixin` must be the first base class in your model's inheritance list (e.g., `class MyModel(LifecycleModelMixin, models.Model):`). Placing `models.Model` before `LifecycleModelMixin` will prevent hooks from firing correctly or lead to unexpected behavior.
- gotcha When using `when` conditions with `has_changed` or `changed_to` for fields storing mutable data (e.g., `JSONField`, `ArrayField` with mutable elements), versions prior to 1.2.0 might not always correctly detect changes due to shallow copy behavior. This could lead to hooks not firing as expected.
- gotcha Custom hook conditions, such as the `condition` decorator or predefined conditions like `is_greater_than`, must be imported from `django_lifecycle.conditions`. Importing them directly from `django_lifecycle` will result in an `ImportError`.
Install
-
pip install django-lifecycle
Imports
- LifecycleModelMixin
from django_lifecycle import LifecycleModelMixin
- hook
from django_lifecycle.hook import hook
from django_lifecycle import hook
- BEFORE_SAVE
from django_lifecycle import BEFORE_SAVE
- AFTER_UPDATE
from django_lifecycle import AFTER_UPDATE
- condition
from django_lifecycle import condition
from django_lifecycle.conditions import condition
- is_greater_than
from django_lifecycle import is_greater_than
from django_lifecycle.conditions import is_greater_than
Quickstart
import os
from django.db import models
from django_lifecycle import LifecycleModelMixin, hook, BEFORE_SAVE, AFTER_UPDATE, POST_INIT
from django_lifecycle.conditions import is_greater_than, is_less_than
# NOTE: This example assumes Django settings are configured, e.g., via manage.py shell
# or a test environment.
class Product(LifecycleModelMixin, models.Model):
name = models.CharField(max_length=255)
price = models.DecimalField(max_digits=10, decimal_places=2, default=0)
stock = models.IntegerField(default=0)
sku = models.CharField(max_length=100, unique=True, blank=True, null=True)
@hook(POST_INIT)
def on_init(self):
if not self.sku:
self.sku = f"SKU-{os.urandom(4).hex().upper()}"
@hook(BEFORE_SAVE)
def ensure_name_capitalized(self):
self.name = self.name.capitalize()
@hook(AFTER_UPDATE, when='price', has_changed=True)
def log_price_change(self):
print(f"Product '{self.name}' (SKU: {self.sku}) price changed from {self.initial_value('price')} to {self.price}")
@hook(BEFORE_SAVE, when='stock', is_greater_than=0, is_less_than=10)
def notify_low_stock(self):
print(f"WARNING: Stock for '{self.name}' (SKU: {self.sku}) is critically low ({self.stock})!")
def __str__(self):
return self.name
# Example Usage (run in a Django shell or similar):
# from your_app.models import Product # Replace 'your_app'
#
# p1 = Product.objects.create(name="keyboard", price=75.00, stock=20)
# print(f"Created: {p1.name} with SKU: {p1.sku}") # SKU will be auto-generated on POST_INIT
#
# p1.price = 80.50
# p1.save() # Triggers log_price_change
#
# p1.stock = 5
# p1.save() # Triggers notify_low_stock
#
# p2 = Product(name="mouse", price=25.00, stock=15)
# p2.save() # Triggers ensure_name_capitalized and on_init (for sku)
# print(f"Created: {p2.name} with SKU: {p2.sku}")