Model Methods, Properties, And Validation
Add behavior to models with __str__, save, delete, get_absolute_url, and custom helpers. Validate at the model level with clean and full_clean. Know when to use @property vs computed fields.
1. Introduction
Models are not just data containers. You can add methods, properties, and validation logic directly to them. This keeps your business logic close to the data it operates on and out of your views.
This guide covers the most important model methods — __str__, save, delete, get_absolute_url, and custom helpers — as well as @property for computed values and model-level validation with clean and full_clean.
- You should already be comfortable defining models with fields and
Metaoptions. - Your
.venvmust be active and Django 5.2 installed.
2. __str__
__str__ returns the human-readable string representation of a model instance. Django uses it in the admin panel, the shell, and anywhere an object is displayed as text. Always define it.
class Article(models.Model):
title = models.CharField(max_length=200)
status = models.CharField(max_length=20)
def __str__(self):
return self.title
# Or include more context
def __str__(self):
return f'{self.title} ({self.status})'
Without __str__, Django falls back to Article object (1) which tells you nothing useful.
3. get_absolute_url
get_absolute_url returns the canonical URL for a model instance. Django uses this in the admin to add a "View on site" button, and it is good practice to define it on any model that has a detail page.
from django.urls import reverse
class Article(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
def get_absolute_url(self):
return reverse('pages:article-detail', kwargs={'slug': self.slug})
Use it in templates to link to the detail page without hardcoding the URL:
<a href=""></a>
4. Overriding save()
Override save() to run custom logic every time a record is saved — for example, auto-generating a slug from the title.
from django.utils.text import slugify
class Article(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=220, unique=True, blank=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super().save(*args, **kwargs)
Two important rules when overriding save():
- Always call
super().save(*args, **kwargs)at the end — without it the record is never actually saved to the database. - Keep the logic simple. Complex logic in
save()can cause unexpected behavior when called from migrations, the shell, or bulk operations.
5. Overriding delete()
Override delete() when you need to run cleanup logic before a record is removed — for example deleting an associated file:
class Profile(models.Model):
photo = models.ImageField(upload_to='profiles/', blank=True)
def delete(self, *args, **kwargs):
if self.photo:
self.photo.delete(save=False)
super().delete(*args, **kwargs)
delete() only runs when you call instance.delete(). It does not run for bulk deletes like Article.objects.filter(...).delete(). For bulk delete cleanup, use signals instead.
6. Custom methods
Add any method that belongs to the model's behavior. This keeps logic out of views and makes it reusable across the project:
class Article(models.Model):
class Status(models.TextChoices):
DRAFT = 'draft', 'Draft'
PUBLISHED = 'published', 'Published'
title = models.CharField(max_length=200)
status = models.CharField(max_length=20, choices=Status, default=Status.DRAFT)
views = models.PositiveIntegerField(default=0)
def is_published(self):
return self.status == self.Status.PUBLISHED
def increment_views(self):
self.views += 1
self.save(update_fields=['views'])
7. @property for computed values
Use @property when a value is derived from other fields and you want to access it like an attribute — without parentheses. Properties are not stored in the database.
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
@property
def word_count(self):
return len(self.content.split())
@property
def is_long(self):
return self.word_count > 1000
Access them like regular attributes:
article.word_count # 342
article.is_long # False
And in templates:
words
8. Model validation with clean()
Use clean() to validate data that involves multiple fields together — something that field-level validators cannot do on their own.
from django.core.exceptions import ValidationError
from django.utils import timezone
class Event(models.Model):
title = models.CharField(max_length=200)
start_date = models.DateTimeField()
end_date = models.DateTimeField()
def clean(self):
if self.start_date and self.end_date:
if self.end_date <= self.start_date:
raise ValidationError('End date must be after start date.')
def __str__(self):
return self.title
clean() is called automatically by Django forms and ModelForm. To run it manually — for example from the shell or a management command — call full_clean():
event = Event(title='Conference', start_date=end, end_date=start)
event.full_clean() # raises ValidationError if clean() fails
event.save()
full_clean() runs field validation, unique checks, and clean() in sequence. Calling save() directly does not run full_clean() automatically — Django leaves this to forms to avoid double validation in form workflows.
9. Putting it together
from django.db import models
from django.urls import reverse
from django.utils.text import slugify
from django.core.exceptions import ValidationError
from django.utils import timezone
class Article(models.Model):
class Status(models.TextChoices):
DRAFT = 'draft', 'Draft'
PUBLISHED = 'published', 'Published'
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=220, unique=True, blank=True)
content = models.TextField()
status = models.CharField(max_length=20, choices=Status, default=Status.DRAFT)
published_at = models.DateTimeField(null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created']
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('pages:article-detail', kwargs={'slug': self.slug})
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super().save(*args, **kwargs)
def clean(self):
if self.status == self.Status.PUBLISHED and not self.published_at:
raise ValidationError('A published article must have a published_at date.')
@property
def word_count(self):
return len(self.content.split())
def is_published(self):
return self.status == self.Status.PUBLISHED
10. Next steps
Your models now have behavior as well as data. The next step covers model constraints — database-level rules that enforce data integrity beyond what field options alone can provide.