Django antipatterns 〉antipattern 〉Use datetime.now as default=… for a created_on field Fork me on GitHub

Often people make use of datetime.now as a default value to specify a field that stores when a record was created, so something like:

from datetime import datetime
from django.db import models

class Post(models.Model):
created_on = models.DateTimeField(default=datetime.now)

# Why is it a problem?

A field is by default editable and not optional. This thus means that if you construct a ModelForm with fields = '__all__', then this will include the created_on in the form. Normally we do not want to include this. While it is of course possible to create a DateTimeField with blank=True and editable=False, but if later additional features arise, one needs to specify more attributes.

Furthermore datetime.now() [python-doc] does not include a timezone. This thus means that the database will store the timestamp without timezone. If thus later the server works with a different timezone, it will render different timestamps.

If one makes use of the freezegun [GitHub], then making use of datetime.now directly will not work. Indeed, if we first define a reference to now(), then freezing the time will not have impact:

>>> from datetime import datetime
>>> from freezegun import freeze_time
>>> nw = datetime.now
>>> with freeze_time('1958-3-25'):
...     print(nw())
...     print(datetime.now())
...
2020-12-06 17:05:35.861048
1958-03-25 00:00:00

so tests with the freezegun will not work.

# What can be done to resolve the problem?

Django's DateTimeField and DateField has a auto_now_add=… parameter [Django-doc]. By setting this parameter to True, you automatically use the current timestamp when you construct the model object. We thus can construct a DateTimeField with auto_now_add=True:

from django.db import models

class Post(models.Model):
created_on = models.DateTimeField(auto_now_add=True)

# Extra tips

Django's DateTimeField and DateField have a auto_now=… parameter [Django-doc] as well. This is a field that will each time take the current timestamp when you update the record, so this can be used for an updated_at field.