Don’t use class methods on Django models
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:
- Instance methods, which are what you get by default when writing a
def
statement inside a class body. These are called from an instance of the model, and Python will ensure that the instance is passed as the first positional argument (conventionally namedself
) when this method is called. - Class methods, which are created by the
@classmethod
decorator. These are called from the class object itself, and Python ensures the class object is passed as the first positional argument (conventionally namedcls
). - Static methods, which are created by the
@staticmethod
decorator. These are called from the class object itself, but no special implicit first argument will be passed to these.
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.