Model Methods, __str__ & Meta

Reviewed & published by Brayan K

A model method is a function defined inside a Django model class that adds behaviour to each row, such as __str__ returning the readable label Django shows for that instance.

Part of the free Django course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll add __str__ and custom instance methods, derive values with @property and get_absolute_url, run logic by overriding save(), and configure table-wide behaviour with class Meta — ordering, verbose names, constraints, and indexes.

What You'll Learn in This Lesson

1 __str__ and Custom Instance Methods

Every Django model is a normal Python class, so you can give it methods. The most important one is __str__: it returns the readable label Django uses in the admin, the shell, and anywhere an instance is printed. Beyond that, you add your own instance methods to express behaviour that belongs to a single row — like checking whether a post is recent.

# blog/models.py
from django.db import models
from django.utils import timezone
from datetime import timedelta

class Post(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()
    created_at = models.DateTimeField(default=timezone.now)

    def __str__(self):
        # The label Django shows for each instance
        return self.title

    def is_recent(self):
        # A custom instance method: behaviour for one row
        return self.created_at >= timezone.now() - timedelta(days=7)

Now str(post) returns the title instead of Post object (1), and post.is_recent() answers a question about that specific row.

# A Django model is just a Python class. __str__ controls the label,
# and a custom method like is_recent() expresses behaviour for one row.
# This plain-Python Post mirrors that exactly.

class Post:
    def __init__(self, title, age_days):
        self.title = title
        self.age_days = age_days        # how many days ago it was created

    def __str__(self):
        # The readable label, just like Django's __str__
        return self.title

    def is_recent(self):
        # A custom instance method: True if created within 7 days
        return self.age_days <= 7

p1 = Post("Hello World", age_days=2)
p2 = Post("Old News", age_days=30)

print("Label:", str(p1))
print("Label:", str(p2))
print(p1.title, "recent?", p1.is_recent())
print(p2.title, "recent?", p2.is_recent())

# EXPECTED:
# Label: Hello World
# Label: Old News
# Hello World recent? True
# Old News recent? False

# ✅ Expected output:
# Label: Hello World
# Label: Old News
# Hello World recent? True
# Old News recent? False

2 @property, get_absolute_url & Overriding save()

A @property turns a method into a read-only attribute, ideal for cheap derived values like a word count. get_absolute_url returns the canonical URL of an object, which Django and the admin use for "View on site" links. Overriding save() lets you run logic on every write — a classic use is auto-generating a slug from the title before calling super().save().

# blog/models.py
from django.db import models
from django.urls import reverse
from django.utils.text import slugify

class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True, blank=True)
    body = models.TextField()

    def __str__(self):
        return self.title

    @property
    def word_count(self):
        # Accessed as post.word_count, no parentheses
        return len(self.body.split())

    def get_absolute_url(self):
        # The canonical URL for this object
        return reverse("post_detail", kwargs={"slug": self.slug})

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)   # auto-set before saving
        super().save(*args, **kwargs)         # actually write the row
# This Post mirrors the Django one: a word_count property, a
# get_absolute_url, and a save() that sets a slug then "persists".

class Post:
    saved_rows = []                      # stands in for the database

    def __init__(self, title, body, slug=""):
        self.title = title
        self.body = body
        self.slug = slug

    @property
    def word_count(self):
        # Like @property in Django: accessed without parentheses
        return len(self.body.split())

    def get_absolute_url(self):
        return "/blog/" + self.slug + "/"

    def slugify(self, text):
        return "-".join(text.lower().split())

    def save(self):
        if not self.slug:
            self.slug = self.slugify(self.title)   # auto-set the slug
        Post.saved_rows.append(self.slug)          # super().save() stand-in
        return self.slug

post = Post("Model Methods Are Great", "one two three four five")
post.save()

print("slug:", post.slug)
print("word_count:", post.word_count)
print("url:", post.get_absolute_url())
print("persisted:", Post.saved_rows)

# EXPECTED:
# slug: model-methods-are-great
# word_count: 5
# url: /blog/model-methods-are-great/
# persisted: ['model-methods-are-great']

# ✅ Expected output:
# slug: model-methods-are-great
# word_count: 5
# url: /blog/model-methods-are-great/
# persisted: ['model-methods-are-great']

3 class Meta: ordering, verbose_name, constraints & indexes

The inner class Meta configures table-wide behaviour that is not a column. ordering sets the default sort, verbose_name and verbose_name_plural control display names, constraints add database-level rules such as UniqueConstraint, and indexes speed up common lookups.

# blog/models.py
from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField()
    author = models.CharField(max_length=100)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]                 # newest first by default
        verbose_name = "blog post"
        verbose_name_plural = "blog posts"
        constraints = [
            models.UniqueConstraint(
                fields=["author", "slug"],
                name="unique_slug_per_author",
            ),
        ]
        indexes = [
            models.Index(fields=["created_at"]),
        ]

    def __str__(self):
        return self.title
# Meta.ordering sets a default sort. Here we reproduce
# ordering = ["-created_at"] (newest first) with sorted(), and check
# a UniqueConstraint on (author, slug) the way the database would.

posts = [
    {"title": "First",  "author": "alice", "slug": "first",  "created_at": 1},
    {"title": "Third",  "author": "bob",   "slug": "third",  "created_at": 3},
    {"title": "Second", "author": "alice", "slug": "second", "created_at": 2},
]

# ordering = ["-created_at"]  ->  newest first
ordered = sorted(posts, key=lambda p: p["created_at"], reverse=True)
print("Default order:")
for p in ordered:
    print(" ", p["title"])

# UniqueConstraint(fields=["author", "slug"])
def violates_unique(rows):
    seen = set()
    for row in rows:
        key = (row["author"], row["slug"])
        if key in seen:
            return True
        seen.add(key)
    return False

print("Unique OK?", not violates_unique(posts))

# EXPECTED:
# Default order:
#   Third
#   Second
#   First
# Unique OK? True

# ✅ Expected output:
# Default order:
#   Third
#   Second
#   First
# Unique OK? True

🎯 Your Turn: Make save() Persist the Row

Fill in the blank so save() sets the slug only when it is empty, mirroring the real if not self.slug: check before super().save().

# 🎯 YOUR TURN: replace ___ so the slug is set only when it is empty.

def slugify(text):
    return "-".join(text.lower().split())

class Post:
    def __init__(self, title, slug=""):
        self.title = title
        self.slug = slug

    def save(self):
        if ___ self.slug:          # only auto-set when slug is empty
            self.slug = slugify(self.title)
        return self.slug

a = Post("Auto Slug Me")
b = Post("Keep My Slug", slug="custom")
print(a.save())
print(b.save())

# EXPECTED OUTPUT:
# auto-slug-me
# custom

Common Errors (And How to Fix Them)

❌ My overridden save() runs but nothing is saved

You forgot to call the parent's save, so the row is never written to the database.

✅ Fix: end your save() with super().save(*args, **kwargs).

❌ TypeError: __str__ returned non-string

Your __str__ returned something like an integer or a model instance.

✅ Fix: wrap it so it always returns text, e.g. return str(self.title).

❌ The admin shows "Categorys" instead of "Categories"

Django guessed the plural by adding an "s", which is wrong for this word.

✅ Fix: set verbose_name_plural = "categories" in class Meta.

📋 Quick Reference

Method / OptionWhat It Does
__str__Readable label for one instance (admin, shell, prints)
@propertyExpose a derived value as an attribute, no parentheses
get_absolute_urlReturn the canonical URL of an object
save()Run logic on write; must call super().save()
class Metaordering, verbose_name, constraints, indexes

🏆 Mini Challenge: Order Like Meta.ordering

Reproduce ordering = ["-created_at", "title"]: sort newest first, and break ties alphabetically by title. Write the sort key.

# 🏆 Mini Challenge: reproduce ordering = ["-created_at", "title"]
# Newest created_at first; ties broken alphabetically by title.

posts = [
    {"title": "Banana", "created_at": 2},
    {"title": "Apple",  "created_at": 2},
    {"title": "Cherry", "created_at": 5},
    {"title": "Date",   "created_at": 1},
]

# -created_at -> descending date; title -> ascending tie-break
ordered = sorted(posts, key=lambda p: (-p["created_at"], p["title"]))

for p in ordered:
    print(p["created_at"], p["title"])

# EXPECTED:
# 5 Cherry
# 2 Apple
# 2 Banana
# 1 Date

❓ Frequently Asked Questions

Lesson complete — your models now have real behaviour!

You can write a readable __str__, add custom instance methods, expose derived values with @property, return a canonical URL with get_absolute_url, run logic by overriding save() and calling super().save(), and configure class Meta with ordering, verbose names, constraints, and indexes.

Practice quiz

What does __str__ return for a model instance?

  • A query result
  • The human-readable label for the instance
  • The primary key
  • An HTML page

Answer: The human-readable label for the instance. __str__ returns the readable label shown in admin, shell, and prints.

Without a custom __str__, how does a Post instance display?

  • Its title
  • Post object (1)
  • An empty string
  • An error

Answer: Post object (1). Without __str__ you see something like 'Post object (1)'.

What type must __str__ always return?

  • An integer
  • A model instance
  • A string
  • A QuerySet

Answer: A string. __str__ must always return a string.

How is a @property accessed on an instance?

  • With parentheses, like post.word_count()
  • Without parentheses, like post.word_count
  • Only in the admin
  • Via the manager

Answer: Without parentheses, like post.word_count. A @property reads like an attribute, without parentheses.

What does get_absolute_url return?

  • The canonical URL of the object
  • The page title
  • The database id
  • A list of URLs

Answer: The canonical URL of the object. get_absolute_url returns the object's canonical URL.

When overriding save(), what must you call so the row is written?

  • self.write()
  • self.commit()
  • models.save()
  • super().save(*args, **kwargs)

Answer: super().save(*args, **kwargs). You must call super().save() to actually persist the row.

What happens if you override save() but forget super().save()?

  • The row is written twice
  • Your logic runs but nothing is saved
  • Django raises a syntax error
  • The model is deleted

Answer: Your logic runs but nothing is saved. Without super().save(), the custom logic runs but no row is written.

Which class Meta option sets the default sort order?

  • sort
  • order_by
  • ordering
  • default_order

Answer: ordering. Meta.ordering sets the default sort, e.g. ['-created_at'].

What does ordering = ['-created_at'] mean?

  • Ascending by created_at
  • No ordering
  • Sort by id
  • Descending by created_at (newest first)

Answer: Descending by created_at (newest first). A leading minus means descending order, newest first.

Which Meta option fixes Django's naive pluralisation in the admin?

  • plural_name
  • name_plural
  • verbose_name_plural
  • verbose_plural

Answer: verbose_name_plural. verbose_name_plural overrides the auto-pluralised display name.

Continue this course

Frequently asked questions

What is the __str__ method on a Django model?

__str__ is a special Python method that returns the human-readable string used to represent a single model instance. Django calls it in the admin, in the shell, and anywhere a model is printed or interpolated into text. Without it you see unhelpful labels like 'Post object (1)'; with it you see something meaningful such as the post's title. It should always return a string and never trigger a database query.

What is the difference between a @property and a regular method?

A regular method is called with parentheses, like post.word_count(), and is the natural choice when the operation takes arguments or clearly does work. A @property is accessed without parentheses, like post.word_count, so it reads like an attribute even though code runs behind the scenes. Properties are ideal for cheap, derived, read-only values such as a computed full name or a word count.

Why do I override save() and call super().save()?

Overriding save() lets you run logic every time an instance is persisted, such as auto-generating a slug from the title or stamping a field. You must call super().save(*args, **kwargs) inside your override, because that is the line that actually writes the row to the database. If you forget it, your custom logic runs but nothing is ever saved.

What does class Meta do on a model?

class Meta is an inner class that configures model-wide behaviour that is not a database column. Common options include ordering to set a default sort, verbose_name and verbose_name_plural to control display names, constraints for database-level rules like UniqueConstraint, and indexes to speed up queries. Meta options change how the table behaves and how the model appears in the admin without adding any fields.