10 Django Secrets No One Talks About (But You Should Know!) [Part 2]
Django is full of hidden gems that even experienced developers might not be aware of. In Part 1, we explored some lesser-known tricks, but…
![10 Django Secrets No One Talks About (But You Should Know!) [Part 2]](/content/images/size/w1200/2025/08/0-6xqcf6hzgomyrjpc.jpg)
Django is full of hidden gems that even experienced developers might not be aware of. In Part 1, we explored some lesser-known tricks, but there’s more to uncover!

Here are 10 more Django secrets that will help you write cleaner, more efficient, and more powerful applications. 🚀
1. You Can Use Database Functions in Querysets
Django provides built-in database functions that let you perform advanced queries without raw SQL.
Example: Case-Insensitive Search with Lower()
from django.db.models.functions import Lower
User.objects.annotate(lower_name=Lower("username")).filter(lower_name="john_doe")
This is more efficient than calling .lower()
function in Python. This will keeps logic inside the database layer for better performance.
Example: Extract Year from a Date
from django.db.models.functions import ExtractYear
BlogPost.objects.annotate(year=ExtractYear("published_date")).filter(year=2024)
This is useful for grouping data by year, month, or day and save it from extra processing in Python.
2. You Can Use bulk_create()
to Insert Multiple Objects at Once
Instead of creating objects one by one, you can insert thousands of records in a single query using bulk_create()
.
Bad Practice: Inserting in a Loop
for i in range(1000):
User.objects.create(username=f"user{i}")
This will executes 1000 separate queries 😱.
Best Practice: Use bulk_create()
users = [User(username=f"user{i}") for i in range(1000)] # create a list of User objects
User.objects.bulk_create(users)
This will give 10x faster performance and it executes only one query instead of 1000 separate ones.
3. Use Subquery()
and OuterRef()
for Advanced Filtering
Django lets you perform subqueries inside Querysets without writing raw SQL.
Example: Get Users with the Latest Order Price
from django.db.models import OuterRef, Subquery, Max
latest_order = Order.objects.filter(user=OuterRef("pk")).order_by("-created_at").values("total")[:1]
User.objects.annotate(latest_order_price=Subquery(latest_order))
This will reduces extra database queries.
4. Optimize get_or_create()
for Better Performance
Using get_or_create()
is great, but it can cause performance issues if misused.
Bad Practice: Calling It Without Filtering
obj, created = User.objects.get_or_create(username="john_doe")
Best Practice: Use defaults
to Avoid Extra Queries
obj, created = User.objects.get_or_create(username="john_doe", defaults={"email": "john@example.com"})
This Avoids unnecessary lookups and Improves performance by using indexed fields.
5. Django’s case
Expression Can Simplify Complex Conditions
Instead of handling conditions in Python, use Case()
inside Querysets.
Example: Assign Labels Based on a Field
from django.db.models import Case, When, Value, CharField
users = User.objects.annotate(
status_label=Case(
When(is_active=True, then=Value("Active")),
When(is_active=False, then=Value("Inactive")),
default=Value("Unknown"),
output_field=CharField(),
)
)
This is Faster than processing in Python and more efficient for large datasets.
6. Use __in_bulk()
for Fast Lookups
When retrieving multiple objects by ID, using filter()
can be inefficient. Instead, use in_bulk()
for faster lookups.
Bad Practice: Using filter()
in a Loop
user_ids = [1, 2, 3, 4, 5]
users = {user.id: user for user in User.objects.filter(id__in=user_ids)}
Best Practice: Use in_bulk()
users = User.objects.in_bulk([1, 2, 3, 4, 5])
This is More efficient than using filter(id__in=...)
and returns a dictionary instead of a queryset.
7. Use Q()
Objects for Complex Queries
When filtering with OR conditions, Django’s Q()
object is a lifesaver.
Example: Search Users by Email OR Username
from django.db.models import Q
users = User.objects.filter(Q(email="test@example.com") | Q(username="test_user"))
This allows more flexible queries and helps when dealing with multiple conditions.
8. Optimize File Uploads with upload_to
Instead of storing all uploads in a single folder, Django lets you define dynamic paths using upload_to
.
Example: Organize User Uploads by ID
def user_directory_path(instance, filename):
return f"user_{instance.user.id}/{filename}"
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
avatar = models.ImageField(upload_to=user_directory_path)
This keeps files organized and Prevents filename conflicts.
9. Speed Up Static File Serving in Development
Django’s default static file handling in development is slow. You can speed it up with WhiteNoise
.
Best Practice: Use WhiteNoise
pip install whitenoise
Modify settings.py
:
MIDDLEWARE.insert(1, "whitenoise.middleware.WhiteNoiseMiddleware")
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
This serves static files more efficiently and no need for runserver
to reload static files.
10. Use reset_queries
for Query Performance Testing
Django keeps track of executed SQL queries. You can reset the counter to measure query performance.
Example: Measure Queries for a Function
from django.db import connection, reset_queries
reset_queries() # Reset query count
users = User.objects.all() # Run some queries
print(len(connection.queries)) # See how many queries were executed
This helps detect unnecessary queries and useful for debugging performance issues.
Final Thoughts
Django has so many hidden secrets that can supercharge your development. The key is to write efficient queries, optimize database performance, and use Django’s built-in features instead of reinventing the wheel.
Which of these Django secrets surprised you the most? Let me know in the comments! 🚀
If you enjoyed this article, I’m sure you’ll love Part 2!

