Behold the power of default – How to set the default argument of a Django model field equal to a class method using lambda

Django model fields accept an optional default argument to set the default value of the field.  This can be either a value or a callable object.  A very common use for the default argument is to set created_at and modified_at datetime fields to datetime.datetime.now.  But default can be much more powerful than that.

You can define your own callables to set a default value.  You might want to write a function that isn’t specific to any particular model class, maybe to generate some special value based upon the state of the entire system or application or even based upon a user.

For example, you could write a function that generates some special kind of random integer and use it to assign a default value in any of your model classes.  In this case your models.py might look like:

 Python |  copy code |? 
1
def my_rand_int_generator():
2
    # return some special random integer
3
 
4
class MyClass(models.Model):
5
    some_random_int = models.IntegerField(default=my_rand_int_generator)

But what if you want to assign a default value that is specific to a particular model class.  Maybe you have two model classes related by a foreign key and want to utilize that relationship to assign a default value.

For example, let’s say you are modeling Invitations and Events.  The Invitation model class has a foreign key relationship to the Event model class.  Your models.py might look like:

 Python |  copy code |? 
1
class Invitation(models.Model):
2
    created_by = models.ForeignKey(User)
3
    event = models.ForeignKey('Event')
4
 
5
class Event(models.Model):
6
    name = models.CharField(max_length=100)
7
    occurs_on = models.DateField('date the event occurs on')
8
    is_a_holiday = models.BooleanField('is the event a holiday?', default=False)

Now let’s say that when a user creates a new Invitation you want the default to be the next Event that is a holiday.  We can easily write a function that returns the next Event that is a holiday, but since the function is specific to the Event model class it should be a class method.  So let’s write that first and then worry about wiring it up to Invitation.event’s default value.

Here is the same Event model class with the addition of a class method next_holiday():

 Python |  copy code |? 
01
class Event(models.Model):
02
    name = models.CharField(max_length=100)
03
    occurs_on = models.DateField('date the event occurs on')
04
    is_a_holiday = models.BooleanField('is the event a holiday?', default=False)
05
 
06
    @classmethod
07
    def next_holiday(cls):
08
        """ Returns the next holiday or None """
09
        try:
10
            next_holiday = Event.objects.filter(occurs_on__gte=datetime.datetime.now, is_a_holiday=True)[0]
11
        except:
12
            next_holiday = None
13
        return next_holiday

Now we can call Event.next_holiday().  Go ahead and try it in a manage.py shell (or django-extension’s much better manage.py shell_plus).  With a working class method for next_holiday(), we can now wire it up to Invitation.event’s default value.  The key is to use lambda, which creates an anonymous function which acts as the callable passed to default.  That anonymous function contains a call to the new class method:

 Python |  copy code |? 
1
class Invitation(models.Model):
2
    created_by = models.ForeignKey(User)
3
    event = models.ForeignKey('Event', default=lambda: Event.next_holiday())

So the final implementation looks like:

 Python |  copy code |? 
01
class Invitation(models.Model):
02
    created_by = models.ForeignKey(User)
03
    event = models.ForeignKey('Event', default=lambda: Event.next_holiday())
04
 
05
class Event(models.Model):
06
    name = models.CharField(max_length=100)
07
    occurs_on = models.DateField('date the event occurs on')
08
    is_a_holiday = models.BooleanField('is the event a holiday?', default=False)
09
 
10
    @classmethod
11
    def next_holiday(cls):
12
        """ Returns the next holiday or None """
13
        try:
14
            next_holiday = Event.objects.filter(occurs_on__gte=datetime.datetime.now, is_a_holiday=True)[0]
15
        except:
16
            next_holiday = None
17
        return next_holiday

So the next time you are overriding a model’s save() method or writing some standalone helper functions that feel hacky, remember about the power of the default argument in conjunction with lambda.


One Response to “Behold the power of default – How to set the default argument of a Django model field equal to a class method using lambda”

Leave a Reply