Skip to content

Don’t use class methods on Django models

Published on: December 17, 2023    Categories: Django, Python

This is part of a series of posts I’m doing as a sort of Python/Django Advent calendar, offering a small tip or piece of information each day from the first Sunday of Advent through Christmas Eve. See the first post for an introduction.

Being methodical about Python

Python classes support three basic types of methods:

The general advice for these is that when you’re writing a class, you almost always want plain instance methods, and you pretty much never want static methods (which are sort of a weird attempt to give Python an equivalent of Java’s static methods, except Java has to have them because it historically didn’t support standalone functions — everything had to be a method of a class — but Python doesn’t have that limitation). And sometimes you want class methods, but they tend to be pretty special case. Most often it’s so you can have alternate constructors. For example, suppose you’re writing a class that represents an RGB color. You might start with:

from dataclasses import dataclass


@dataclass
class RGBColor:
    red: int
    green: int
    blue: int

This will get an automatic constructor (courtesy of the dataclass helper) which takes three arguments and sets them as the values of the red, green, and blue attributes. Then you might decide you also want to support creating an RGBColor from a hexadecimal string like #daa520 or #000080. So you could add an alternate constructor which will parse out the integer values from the hex, and it’d be best to mark this as a classmethod:

from dataclasses import dataclass


@dataclass
class RGBColor:
    red: int
    green: int
    blue: int

    @classmethod
    def from_hex(cls, hex_str: str):
        # Put the logic to parse out the red, green, blue components
        # here...
        #
        # Then make the new instance and return it:
        return cls(red, green, blue)

But what’s that about Django?

Except you shouldn’t do this with Django models. You can, and it’ll work, and it won’t break anything, but generally a Django model class should contain only instance-level definitions and behaviors; class-level behavior should be defined on a model’s manager class.

This is because Django already puts class-level (or whole-table-level, if you’re thinking in SQL terms) behavior on the manager automatically, and it’s conceptually simpler to be consistent about the class-level versus instance-level manager/model split than to have some class-level behavior on the model and some on the manager.

So a similar example in the Django ORM should look like:

from django.db import models


class RGBColorManager(models.Manager):
    def from_hex(self, hex_str):
        # Put the logic to parse out the red, green, blue components
        # here...
        #
        # Then make the new instance and return it:
        return self.model(red, green, blue)


class RGBColor(models.Model):
    red = models.IntegerField()
    green = models.IntegerField()
    blue = models.IntegerField()

    objects = RGBColorManager()

And then you’d call it like: RGBColor.objects.from_hex("#daa520").

This model probably also wants to have some validators attached to its fields to enforce that the red, green, and blue values are all in the permitted 0-255 range, but that’s orthogonal to where to put the alternate constructor.