pip install django-doctor

Django Doctor suggests code improvements to help you build world-class websites, release faster, and reduce dev costs. Example:

template.htmlmaintainabilitymedium
-
<img src="/static/logo.png" />
+
{% load static %}
+
<img src="{% static 'logo.png' %}" />

Hard-coding static asset urls is brittle because the place the files are stored depends on the `STATICFILES_STORAGE` used - so if in prod the storage backend uploads to S3 or even renames the file then this hard-coded URL will break.

Using "{% static ... %}" solves that as it knows exactly where the files are stored.

Read more
Join over 3000 teams already using Django Doctor
  • Department for International Trade
  • Giant
  • Motley fool
  • Lift Interactive
  • Lightmatter
  • Media Interactive
  • We Make Services
  • Sumo

"Extremely positive. Django Doctor suggested useful changes, giving our senior developers time back."

Jon Atkinson, Technical Director at Giant

Enhance your security

Protect your business and users against common vulnerabilities including missing Django fixes, XSS, Cross Site Request Forgery, clickjacking, session cookie hijacking, and more.
settings.py
+
from ast import literal_eval
+
from os import getenv
+
MIDDLEWARE = [
    'django.middleware.csrf.CsrfViewMiddleware',
    ...
]
+
+
# feature flagged so can turn off in local development
+
CSRF_COOKIE_SECURE = literal_eval(getenv("HTTPS_ONLY", "True"))

Your website is vulnerable because the CSRF_COOKIE_SECURE setting is not set - so hackers have an easier time stealing your CSRF cookies on HTTP connections, allowing them to circumvent your CSRF protection.

Read more
settings.py
MIDDLEWARE = [
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
+
    'django.middleware.csrf.CsrfViewMiddleware',
    ...
]

Your website is vulnerable to a number of common hacker attacks because MIDDLEWARE setting is missing django.middleware.security.SecurityMiddleware.

Read more
settings.py
MIDDLEWARE = [
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
+
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    ...
]

Your website is vulnerable to clickjack attack because the MIDDLEWARE setting is missing django.middleware.clickjacking.XFrameOptionsMiddleware - so users can be tricked into interacting with your website without realising.

Read more
settings.py
-
SESSION_COOKIE_SECURE = False
+
from ast import literal_eval
+
from os import getenv
+
+
# feature flagged so can turn off in local development
+
SESSION_COOKIE_SECURE = literal_eval(getenv("HTTPS_ONLY", "True"))

Your website is vulnerable because the SESSION_COOKIE_SECURE setting is not set - so hackers have an easier time stealing your users' session cookies on HTTP connections.

Read more

Improve your website's speed

Over half of Django websites are slowed by unoptimized code. Website speed has a direct impact on the success of your organization. Protect against unoptimized code that at best lose you customers and at worse bring your website down. Read more.
tasks.py
def trigger_tasks(queryset):
    for item in queryset:
-
        run_async_task(item.page.id)
+
        run_async_task(item.page_id)

When working with foreign keys, accessing the related field will result in a database read. That can be eliminated by using *_id, which is the foreign key value that Django has already cached on the object to make this scenario more efficient.

Read more
views.py
+
from random import randint
+
def get_random_item():
-
    return MyModel.objects.order_by('?')[0]
+
    index = randint(0, MyModel.objects.count() -1)
+
    return model.objects.all()[index]

Using order_by('?') can be very inefficient if you have lots of rows in the table. Moving the randomness to the application layer will probably give significant a performance improvement.

Read more
tasks.py
def task(admin_blog_ids):
    queryset = CommentModel.objects.all()
-
    if len(queryset) > 100:
+
    if queryset.count() > 100:
        send_email_to_admin(queryset)

len(queryset) performs the count at application level. That is much less efficient than doing queryset.count(), which does the calculation at database level and just returns the count.

Read more
helpers.py
def trigger_task(queryset):
-
    if queryset.count() > 0:
+
    if queryset.exists():
        trigger_tasks(queryset)

Comparing queryset.count() is less efficient than checking queryset.exists(), so use querySet.count() if you only want the count, and use queryset.exists() if you only want to find out if at least one result exists.

Read more

Ensure maintainability best practices

Automatically find and fix over 40 issues, so your team can focus on adding value. Reduce costs by preventing tech debt including hidden 404s, migrations blocking production rollback, hard-coded urls, and more.
template.html
-
<img src="/static/logo.png" />
+
{% load static %}
+
<img src="{% static 'logo.png' %}" />

Hard-coding static asset urls is brittle because the place the files are stored depends on the `STATICFILES_STORAGE` used - so if in prod the storage backend uploads to S3 or even renames the file then this hard-coded URL will break.

Using "{% static ... %}" solves that as it knows exactly where the files are stored.

Read more
migrations/0002_auto.py
class Migration(migrations.Migration):
    dependencies = [("core", "0001_initial.py")]
-
    operations = [RunPython(forwards)]
+
    operations = [RunPython(forwards, RunPython.noop)]

It's good to, as a minimum, specify noop in RunPython so the migration can be skipped when going backwards, and even better to specify a function that undoes the migration.

Read more
tasks.py
-
from models import MyModel
-
 
def forwards(apps, schema_editor):
-
    MyModel.objects.get(...)
+
    apps.get_model("core", "MyModel").objects.get(...)

It's better to use apps.get_model, which guarantees the Model's fields will reflect the fields in the database even if models.py is vastly out of step with the database.

Read more
models.py
class CommentModel(models.Model):
-
    is_active = models.NullBooleanField()
+
    is_active = models.BooleanField(null=True)
    body = models.TextField()

NullBooleanField is deprecated and will be removed a future version of Django.

Read more

Self hosted price

Individual

Free

forever

CI integration: Yes

Improvement engine: Yes

Number of users: 1

Support: No

Commercial use: No

Copied to clipboard.
Popular

Team

$49

per month

CI integration: Yes

Improvement engine: Yes

Number of users: 10

Support: 24/7 email and chat

Commercial use: Yes

Copied to clipboard.

Organization

$99

per month

CI integration: Yes

Improvement engine: Yes

Number of users: 30 (contact for more)

Support: 24/7 email and chat

Commercial use: Yes

Copied to clipboard.