Senior Tips & Tricks
A grab-bag of the small, high-leverage habits that mark an experienced Django developer.
None of these is big enough for its own lesson, but together they are the difference between code that works and code a senior wrote. Keep this list on hand during code review.
ORM & performance
- Use
.only()/.defer()to fetch just the columns you need on wide tables. - Use
.values()/.values_list()when you need dicts or tuples — skip building model instances. bulk_create/bulk_updatefor many rows; they turn N inserts into one round-trip.exists()beatslen(qs) > 0;count()beatslen(qs.all()).get_or_createandupdate_or_createcollapse the read-then-write dance — and are atomic.
Debugging & correctness
# See the actual SQL a queryset will run:
print(qs.query)
# Count queries a block makes (great in tests):
from django.test.utils import CaptureQueriesContext
from django.db import connection
with CaptureQueriesContext(connection) as ctx:
list(Post.objects.select_related('author'))
print(len(ctx)) # your query budgetProject hygiene
- Pin dependencies and run
pip-auditin CI. - Reference the user model with
settings.AUTH_USER_MODEL(in models) orget_user_model()(in code) — never importUserdirectly. - Keep business logic out of views: a thin view calling a service function or a fat model method tests far more easily.
Example
from django.contrib.auth import get_user_model
from django.db import transaction
User = get_user_model() # never 'from django.contrib.auth.models import User'
# 1) get_or_create / update_or_create — atomic upserts, no race.
tag, created = Tag.objects.get_or_create(
slug='django',
defaults={'name': 'Django'}, # only used if it has to create
)
profile, _ = Profile.objects.update_or_create(
user=user,
defaults={'last_seen': timezone.now()},
)
# 2) Fetch dicts, not model instances, for read-only list endpoints.
rows = (
Order.objects
.filter(status='paid')
.values('id', 'customer__email', 'total') # follows the FK, one query
)
# 3) Bulk operations: one round-trip instead of thousands.
with transaction.atomic():
Product.objects.bulk_create(
[Product(sku=s, name=n) for s, n in incoming],
batch_size=500,
ignore_conflicts=True,
)
to_touch = list(Product.objects.filter(category='clearance'))
for p in to_touch:
p.price *= 0.8
Product.objects.bulk_update(to_touch, ['price'], batch_size=500)
# 4) Thin view -> service function: the logic is unit-testable without HTTP.
def checkout_view(request):
order = services.place_order(user=request.user, cart=request.session['cart'])
return redirect('order-detail', pk=order.pk)
# services.py holds place_order() — pure Python, no request object,
# trivial to test and reuse from a management command or a Celery task.When to use it
- A developer uses update_fields=['status'] in save() to update only a single column instead of all model fields.
- A developer uses bulk_create() to insert thousands of records in a single database call instead of a Python loop.
- A team uses django-debug-toolbar during development to identify slow queries and unexpected N+1 patterns.
More examples
update_fields for targeted saves
Saves only the specified fields to the database, avoiding unnecessary writes and concurrency bugs.
post = Post.objects.get(pk=1)
post.status = "published"
post.save(update_fields=["status", "updated_at"])
# Emits: UPDATE blog_post SET status=... WHERE id=1bulk_create for mass inserts
Inserts a list of model instances in a single SQL statement; ignore_conflicts skips duplicates.
tags = [Tag(name=name) for name in ["python", "django", "rest"]]
Tag.objects.bulk_create(tags, ignore_conflicts=True)
# One INSERT statement for all tagsonly() and defer() for lighter queries
Limits the columns fetched from the database to reduce query payload for list views.
# Only fetch id and title; skip large body column
posts = Post.objects.only("id", "title").order_by("-created_at")[:20]
# Or defer the heavy column explicitly
posts = Post.objects.defer("body").all()
Discussion