Senior Tips & Tricks
A grab-bag of the habits, shortcuts and gotchas that separate a seasoned Django developer from someone who just knows the docs.
None of these is big enough for its own chapter, but together they are the difference between code that works and code a senior would sign off on. Consider this your field notebook.
Query and data
- See the SQL.
print(qs.query)shows the exact SQL a queryset will run — your first move when a query behaves oddly. .only()/.defer()fetch or skip specific columns when a table has a fatTextFieldyou rarely need.bulk_create/bulk_updatecollapse thousands of INSERTs into one round-trip.update()anddelete()on a queryset skipsave()and signals — fast, but know what you are bypassing.get_or_create/update_or_createhandle the "insert if missing" race cleanly — back them with a unique constraint.
Debugging and shell
manage.py shell_plus(django-extensions) auto-imports every model. Add--print-sqlto watch every query.- Django Debug Toolbar in dev makes N+1 problems visible at a glance.
Correctness habits
- Timezone-aware datetimes: always
from django.utils import timezone; timezone.now(), neverdatetime.now(). update_fieldsonsave()writes only what changed — less contention, and it avoids clobbering a field another process just updated.- Reverse URLs, never hardcode.
reverse("order-detail", args=[pk])and{% url %}survive route changes; string paths do not.
The meta-tip: optimise for the reader. The Django that ages well is boring, explicit, and easy to delete. Clever is a liability the next person — often future-you — has to reverse-engineer at 2am.
Example
from django.utils import timezone
from django.urls import reverse
from .models import Article, Tag
# --- See the actual SQL a queryset will run (do this FIRST when confused) ---
qs = Article.objects.filter(status="published").select_related("author")
print(qs.query) # SELECT ... FROM blog_article INNER JOIN ... WHERE status = 'published'
# --- Fetch only the columns you need; skip a fat body column ---
headlines = Article.objects.only("id", "title").filter(status="published")
light = Article.objects.defer("body") # everything except the big TextField
# --- One round-trip instead of thousands ---
Tag.objects.bulk_create([Tag(name=n) for n in ("django", "orm", "async")])
Article.objects.filter(views__gt=1000).update(featured=True) # skips save()/signals
# --- Race-safe upsert, backed by a unique constraint ---
tag, created = Tag.objects.get_or_create(name="python", defaults={"color": "blue"})
# --- Always timezone-aware, always reverse() ---
article = Article.objects.create(title="Hi", published_at=timezone.now())
url = reverse("article-detail", args=[article.pk]) # survives URL refactors
# --- Write only what changed: cheaper, avoids clobbering concurrent updates ---
article.title = "Edited"
article.save(update_fields=["title", "updated_at"])When to use it
- A developer uses select_for_update() inside a transaction to prevent race conditions in concurrent purchase flows.
- A team uses django-extensions's shell_plus to get auto-imported models and utilities in the Django shell.
- A developer uses Django's system check framework to write a custom check that validates required environment variables at startup.
More examples
Use only() to reduce column reads
Fetches only the columns needed for a feed page, avoiding the large body column and reducing query payload.
# Only fetch needed columns for a feed
posts = (
Post.objects
.only("id", "title", "slug", "published_at", "author_id")
.select_related("author")
.filter(draft=False)
.order_by("-published_at")[:25]
)Custom system check
A custom system check that reports missing environment variables when running any manage.py command.
from django.core.checks import Error, register
import os
@register()
def check_required_env_vars(app_configs, **kwargs):
errors = []
required = ["STRIPE_SECRET_KEY", "SENDGRID_API_KEY"]
for var in required:
if not os.environ.get(var):
errors.append(Error(f"{var} is not set", id="myapp.E001"))
return errorsReusable test fixture factory
Uses factory_boy to define reusable test data factories so tests create realistic objects with minimal code.
# tests/factories.py using factory_boy
import factory
from django.contrib.auth import get_user_model
from blog.models import Post
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = get_user_model()
username = factory.Sequence(lambda n: f"user{n}")
email = factory.LazyAttribute(lambda u: f"{u.username}@example.com")
class PostFactory(factory.django.DjangoModelFactory):
class Meta:
model = Post
title = factory.Faker("sentence", nb_words=5)
author = factory.SubFactory(UserFactory)
Discussion