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_update for many rows; they turn N inserts into one round-trip.
  • exists() beats len(qs) > 0; count() beats len(qs.all()).
  • get_or_create and update_or_create collapse 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 budget

Project hygiene

  • Pin dependencies and run pip-audit in CI.
  • Reference the user model with settings.AUTH_USER_MODEL (in models) or get_user_model() (in code) — never import User directly.
  • Keep business logic out of views: a thin view calling a service function or a fat model method tests far more easily.

Example

Example · python
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.

Example · python
post = Post.objects.get(pk=1)
post.status = "published"
post.save(update_fields=["status", "updated_at"])
# Emits: UPDATE blog_post SET status=... WHERE id=1

bulk_create for mass inserts

Inserts a list of model instances in a single SQL statement; ignore_conflicts skips duplicates.

Example · python
tags = [Tag(name=name) for name in ["python", "django", "rest"]]
Tag.objects.bulk_create(tags, ignore_conflicts=True)
# One INSERT statement for all tags

only() and defer() for lighter queries

Limits the columns fetched from the database to reduce query payload for list views.

Example · python
# 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

  • Be the first to comment on this lesson.