Django: Use auto_now and auto_now_add Correctly — When to Switch to default=timezone.now
Most Django models have two timestamp fields: created_at to know when a record was created and updated_at for the last edit. The quickest way is auto_now_add=True for created_at and auto_now=True for updated_at — Django
Most Django models have two timestamp fields: created_at to know when a record was created and updated_at for the last edit. The quickest way is auto_now_add=True for created_at and auto_now=True for updated_at — Django fills in both and you never touch them. But these two options have three common "traps": you can't assign an old date when importing data, bulk update() doesn't change updated_at, and the fields are hidden from forms. This post walks through each trap and how to handle it.
The Problem
Suppose you import 500 old posts from another blog and want to keep their original publish dates. You write:
from datetime import datetime
from django.utils import timezone
old_date = timezone.make_aware(datetime(2024, 5, 10, 9, 0))
# Muốn giữ ngày đăng gốc khi import...
post = Post.objects.create(title="Bài cũ", created_at=old_date)
print(post.created_at) # Hôm nay, không phải 10/05/2024!
Result: created_at is overwritten with the current time and the original date is lost. Reason: auto_now_add only runs on INSERT — any value you pass to create() is replaced with timezone.now().
The second trap is in bulk updates. You change the status of 1,000 posts:
from django.utils import timezone
# Bẫy thứ hai: cập nhật hàng loạt
Post.objects.filter(status="draft").update(status="published")
# updated_at của 1.000 bài viết vẫn giữ nguyên!
The updated_at of all posts stays unchanged — because QuerySet.update() runs SQL directly, without calling save(), so auto_now never fires. Users looking at "last updated" will see an old date even though the content just changed.
The Solution
To keep the original date when importing, don't use auto_now_add for created_at. Use default=timezone.now instead — note you pass the function, not call it:
from django.db import models
from django.utils import timezone
class Post(models.Model):
title = models.CharField(max_length=200)
status = models.CharField(max_length=20, default="draft")
created_at = models.DateTimeField(default=timezone.now)
updated_at = models.DateTimeField(default=timezone.now)
With this approach, creating a record without passing created_at still auto-fills the current time, while importing lets you pass the old date and keep it. These two fields remain editable in the admin and ModelForm, unlike auto_now/auto_now_add, which are hidden.
For bulk updates, pass updated_at explicitly:
from django.utils import timezone
Post.objects.filter(status="draft").update(
status="published",
updated_at=timezone.now(),
)
If you still want to keep auto_now_add because you never need to edit created_at, you can — just assign the old date after creating: create the record first, assign created_at, then save() again. Since auto_now_add only runs on INSERT, the second save() doesn't overwrite it.
post = Post.objects.create(title="Bài cũ")
post.created_at = old_date # Gán sau khi tạo thì giữ được
post.save()
Apply It to Your Project
Quick rule: use auto_now_add/auto_now for tables that only need a time "stamp", like logs, activity history, or posts whose date you never edit. Use default=timezone.now when the date is business data — post publish date, expiry date, event start date — because sooner or later you'll need to edit it by hand, import old data, or let users change it in the admin. And after every QuerySet.update() or bulk_update(), check whether updated_at needs an explicit update.
Related Posts
- Django: sửa lỗi N+1 query bằng select_related trong 5 phút
- Django: dùng get_or_create() và update_or_create() đúng cách — lấy hay tạo bản ghi chỉ trong 1 dòng
- Django: tạo và cập nhật hàng loạt với bulk_create/bulk_update thay vì save() trong vòng lặp
Frequently Asked Questions
How do auto_now and auto_now_add differ?
auto_now_add only runs when creating (INSERT); auto_now runs on every save(). Both auto-fill timezone.now() and can't be edited through forms.
Why doesn't update() change updated_at?
update() runs SQL directly, skipping save(), so auto_now's pre_save never runs. Pass updated_at=timezone.now() into update() to change it explicitly.
How do default=timezone.now and default=timezone.now() differ?
Passing the function (no parentheses) makes Django call it again for each new record, so every record gets its own time. Calling the function upfront (with parentheses) makes all records share one moment computed when the module was imported — a classic bug.
Should I use auto_now for a post's publish date?
No, if the publish date is business data you need to edit by hand or import. Use default=timezone.now so it auto-fills and stays editable.
Does bulk_update() refresh auto_now?
No, same as update(). Pass updated_at explicitly or update each record with save().
💬 Have you tried this in your project? If you hit an error or have a neater way, leave a comment below — I read them all and will add the best ideas to the post.
References
- Django docs — DateField.auto_now / auto_now_add: docs.djangoproject.com/en/6.1/ref/models/fields/#django.db.models.DateField.auto_now
- Django docs — QuerySet.update(): docs.djangoproject.com/en/6.1/ref/models/querysets/#update
- Django docs — django.utils.timezone.now: docs.djangoproject.com/en/6.1/ref/utils/#django.utils.timezone.now
Originally published at linhfish.dev.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.