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 Meta options.
  • Your .venv must 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)

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()

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.


Never miss a story on Django.wiki

Subscribe for fresh tutorials, snippets, and updates.

By subscribing you agree to our Privacy Policy.