← Back to list

Django Code Smells: The 30 Patterns That Signal Trouble

Spot problems in seconds, not hours

Anas Issath · 2025-12-19 20:29 · 78 claps · 11.8 min read paywalled
#django #django-best-practices #code-quality #code-review #backend-development
Open on Medium ↗
Wiki topics: 🌐 · Web Development

Django Code Smells: The 30 Patterns That Signal Trouble

Spot problems in seconds, not hours

Photo by Richárd Ecsedi on Unsplash

Photo by Richárd Ecsedi on Unsplash

If you’re not a Medium member, you can read this article for free via this link: Friend Link

I’ve reviewed 1,000+ Django pull requests.

Here’s what I’ve learned:

Most bugs have a smell — a pattern that signals “something’s wrong here.”

The difference between junior and senior:

Junior: “This code works, ship it!” Senior: “This code smells. Let me investigate.”

The pattern:

  • Junior spends 2 hours coding, 10 hours debugging
  • Senior spends 10 minutes spotting smells, prevents the bugs

The truth:

  • Every bug has warning signs
  • Code smells predict future problems
  • Pattern recognition beats deep analysis
  • Trust your nose

Today, I’ll teach you to smell trouble instantly.

30 patterns that signal problems. Each with:

  • ✅ The smell (what to look for)
  • ✅ Why it’s bad
  • ✅ How to fix it
  • ✅ When it’s acceptable

No judgment. Just recognition.

Let’s sharpen your senses.

Category 1: Database Smells

Smell #1: The Loop Query

What you see:

posts = Post.objects.all()
for post in posts:
    author = post.author  # Query!
    print(f"{author.name}: {post.title}")

The smell: Database query inside a loop

Why it’s bad:

1 post → 2 queries
10 posts → 11 queries
100 posts → 101 queries (N+1 problem)

Impact:

  • 50ms → 5 seconds
  • Database CPU: 80%
  • Kills performance at scale

How to fix:

posts = Post.objects.select_related('author')
for post in posts:
    author = post.author  # No query! Already loaded
    print(f"{author.name}: {post.title}")

# 1 post → 1 query
# 100 posts → 1 query

When it’s acceptable:

  • Loop runs once or twice
  • Prototyping (fix before production)

Detection tip:

# In tests, check query count
from django.test.utils import override_settings
from django.db import connection

with self.assertNumQueries(1):  # Expect 1 query
    list(Post.objects.select_related('author'))

Smell #2: The Missing Index

What you see:

class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField()  # No db_index=True
    status = models.CharField(max_length=20)

# Later in code:
Post.objects.filter(slug='my-post')  # Slow!
Post.objects.filter(status='published')  # Full table scan!

The smell: Filtering/ordering by field without index

Why it’s bad:

Without index: O(n) - full table scan
With index: O(log n) - fast lookup

1,000 rows: 10x slower
1,000,000 rows: 1000x slower

How to fix:

class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True, db_index=True)  # ✓
    status = models.CharField(max_length=20, db_index=True)  # ✓
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)  # ✓

    class Meta:
        indexes = [
            # Composite index for common query
            models.Index(fields=['status', '-created_at']),
        ]

Rule of thumb: Index if you:

  • Filter by it (filter(field=value))
  • Order by it (order_by('field'))
  • Use it in joins (select_related('field'))

When NOT to index:

  • Field rarely queried
  • Small table (<1000 rows)
  • Field has low cardinality (2–3 values)

Smell #3: The Lazy Delete

What you see:

# Deleting without consideration
post.delete()
user.delete()
order.delete()

The smell: No cascade strategy defined

Why it’s bad:

# What happens to related objects?
# Comments? Likes? Analytics?
# Often causes:
# - Data integrity issues
# - Broken foreign keys
# - Lost historical data

How to fix:

class Comment(models.Model):
    post = models.ForeignKey(
        Post,
        on_delete=models.CASCADE  # ✓ Explicit strategy
    )
    author = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,  # ✓ Keep comment, remove author
        null=True
    )

class Order(models.Model):
    user = models.ForeignKey(
        User,
        on_delete=models.PROTECT  # ✓ Can't delete if has orders
    )

Cascade strategies:

CASCADE      # Delete related objects
PROTECT      # Prevent deletion if related exist
SET_NULL     # Set to NULL (keep record)
SET_DEFAULT  # Set to default value
DO_NOTHING   # Database handles it (dangerous)
SET(func)    # Set to function result

When it’s acceptable:

  • Prototyping only
  • Test data

Smell #4: The Fat Model

What you see:

class User(models.Model):
    # Fields...

    def send_welcome_email(self):
        # 30 lines of email logic

    def charge_credit_card(self):
        # 40 lines of Stripe integration

    def generate_analytics_report(self):
        # 50 lines of report logic

    def sync_to_crm(self):
        # 30 lines of API calls

    # 500 lines total in model

The smell: Model has business logic, external API calls, complex operations

Why it’s bad:

  • Hard to test (model tied to everything)
  • Slow to load (imports everything)
  • Violates single responsibility
  • Can’t reuse logic outside model

How to fix:

# models.py - Keep it simple
class User(models.Model):
    email = models.EmailField()
    # Just data definition

# services/email.py
class EmailService:
    @staticmethod
    def send_welcome_email(user):
        # Email logic here

# services/payment.py
class PaymentService:
    @staticmethod
    def charge_card(user, amount):
        # Stripe logic here

# Usage
from services.email import EmailService
EmailService.send_welcome_email(user)

Rule of thumb: Models should:

  • ✓ Define data structure
  • ✓ Simple property methods
  • ✓ Basic validation

Models should NOT:

  • ✗ Call external APIs
  • ✗ Send emails
  • ✗ Generate reports
  • ✗ Complex business logic

Smell #5: The String Query

What you see:

# Using raw SQL strings
User.objects.raw('SELECT * FROM users WHERE status = %s', ['active'])

# Or worse
from django.db import connection
cursor = connection.cursor()
cursor.execute("SELECT * FROM posts WHERE author_id = %s", [user_id])

The smell: Raw SQL when ORM would work

Why it’s bad:

  • SQL injection risk
  • Loses ORM benefits (caching, relations)
  • Database-specific (not portable)
  • Harder to test
  • Breaks IDE autocomplete

How to fix:

# Use ORM
users = User.objects.filter(status='active')

# If complex query needed
from django.db.models import Q, F, Count

posts = Post.objects.filter(
    Q(status='published') & 
    Q(views__gt=F('author__follower_count'))
).annotate(
    comment_count=Count('comments')
)

When raw SQL is acceptable:

  • Complex aggregations ORM can’t handle
  • Performance-critical queries (after profiling)
  • Database-specific features needed
  • Migrations

If you must use raw SQL:

# Use parameterized queries (not string concatenation)
User.objects.raw(
    'SELECT * FROM users WHERE status = %s',
    ['active']  # ✓ Parameterized
)

# NOT this:
User.objects.raw(
    f'SELECT * FROM users WHERE status = {status}'  # ✗ Injection risk!
)

Category 2: View Smells

Smell #6: The God View

What you see:

def dashboard(request):
    # 200 lines of code
    # 15 database queries
    # 5 external API calls
    # 3 email sends
    # Multiple try/except blocks
    # Returns one template

The smell: View doing everything

Why it’s bad:

  • Hard to test
  • Hard to maintain
  • Hard to reuse logic
  • Slow response time
  • Hard to debug

How to fix:

# Break into smaller pieces
def dashboard(request):
    context = {
        'stats': get_user_stats(request.user),
        'notifications': get_notifications(request.user),
        'recent_activity': get_recent_activity(request.user),
    }
    return render(request, 'dashboard.html', context)

# Separate functions
def get_user_stats(user):
    # Focused logic
    return {...}

def get_notifications(user):
    # Focused logic
    return [...]

def get_recent_activity(user):
    # Focused logic
    return [...]

Rule of thumb: Views should:

  • ✓ Handle request/response
  • ✓ Call services
  • ✓ Return template/JSON
  • ✓ Stay under 50 lines

Views should NOT:

  • ✗ Contain business logic
  • ✗ Make direct external calls
  • ✗ Have complex algorithms
  • ✗ Be >100 lines

Smell #7: The Template Logic

What you see:

<!-- template.html -->
{% for post in posts %}
    {% if post.published and post.author.is_active and not post.is_draft %}
        {% if post.view_count > 100 %}
            Popular: {{ post.title }}
        {% else %}
            {{ post.title }}
        {% endif %}
    {% endif %}
{% endfor %}

The smell: Complex logic in templates

Why it’s bad:

  • Hard to test
  • Slow template rendering
  • Logic duplicated across templates
  • Hard to maintain

How to fix:

# In view or model method
class Post(models.Model):
    # ...

    def is_visible(self):
        return (
            self.published and 
            self.author.is_active and 
            not self.is_draft
        )

    def is_popular(self):
        return self.view_count > 100

# Or in view
def blog_list(request):
    posts = Post.objects.filter(
        published=True,
        author__is_active=True,
        is_draft=False
    )
    return render(request, 'blog.html', {'posts': posts})
<!-- template.html - Clean! -->
{% for post in posts %}
    {% if post.is_popular %}
        Popular: {{ post.title }}
    {% else %}
        {{ post.title }}
    {% endif %}
{% endfor %}

Rule of thumb: Templates should:

  • ✓ Display data
  • ✓ Simple formatting
  • ✓ Basic conditionals (show/hide)

Templates should NOT:

  • ✗ Business logic
  • ✗ Complex calculations
  • ✗ Database queries
  • ✗ Nested conditions (>2 levels)

Smell #8: The Missing CSRF

What you see:

@csrf_exempt  # Danger!
def api_endpoint(request):
    if request.method == 'POST':
        # Process data
        pass

The smell: @csrf_exempt decorator

Why it’s bad:

  • CSRF vulnerability
  • Easy to exploit
  • Data can be manipulated
  • Security audit failure

How to fix:

# Option 1: Use CSRF (default)
def api_endpoint(request):
    # Django handles CSRF automatically
    pass

# Option 2: If building API, use proper auth
from rest_framework.decorators import api_view
from rest_framework.permissions import IsAuthenticated

@api_view(['POST'])
@permission_classes([IsAuthenticated])
def api_endpoint(request):
    # Token authentication, not CSRF
    pass

When @csrf_exempt is acceptable:

  • Public webhook endpoints (verify signature instead)
  • API with token authentication (not cookie-based)
  • Third-party callbacks

If you must exempt:

@csrf_exempt
def webhook(request):
    # Verify signature instead
    signature = request.META.get('HTTP_X_SIGNATURE')
    if not verify_signature(request.body, signature):
        return HttpResponse(status=403)
    # Process webhook

Smell #9: The Silent Exception

What you see:

def process_payment(request):
    try:
        charge_card(request.user, amount)
        send_confirmation_email(request.user)
        update_inventory(product_id)
    except Exception:
        pass  # Silent failure!

    return HttpResponse("Success!")  # Lies!

The smell: Empty except block

Why it’s bad:

  • Errors hidden
  • Bugs undetected
  • False success messages
  • Impossible to debug
  • Data inconsistency

How to fix:

import logging
logger = logging.getLogger(__name__)

def process_payment(request):
    try:
        charge_card(request.user, amount)
        send_confirmation_email(request.user)
        update_inventory(product_id)
        return HttpResponse("Success!")
    except PaymentError as e:
        logger.error(f"Payment failed: {e}", exc_info=True)
        return HttpResponse("Payment failed", status=400)
    except EmailError as e:
        # Payment succeeded but email failed
        logger.warning(f"Email failed: {e}", exc_info=True)
        return HttpResponse("Success! (Email pending)")
    except Exception as e:
        # Unexpected error
        logger.critical(f"Unexpected error: {e}", exc_info=True)
        return HttpResponse("Error", status=500)

Rule of thumb:

  • ✗ Never use bare except:
  • ✗ Never use empty except block
  • ✓ Catch specific exceptions
  • ✓ Log errors
  • ✓ Return appropriate response

Smell #10: The Dirty Form

What you see:

def signup(request):
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        email = request.POST.get('email')

        # Manual validation
        if not username or len(username) < 3:
            return HttpResponse("Username too short")
        if not password or len(password) < 8:
            return HttpResponse("Password too short")
        # ... 50 more lines of validation

        User.objects.create_user(username, email, password)

The smell: Manual form validation instead of Django forms

Why it’s bad:

  • Validation logic scattered
  • Easy to miss edge cases
  • No CSRF by default
  • Hard to test
  • Can’t reuse validation
  • No error messages on fields

How to fix:

# forms.py
from django import forms

class SignupForm(forms.Form):
    username = forms.CharField(min_length=3, max_length=30)
    email = forms.EmailField()
    password = forms.CharField(min_length=8, widget=forms.PasswordInput)

    def clean_username(self):
        username = self.cleaned_data['username']
        if User.objects.filter(username=username).exists():
            raise forms.ValidationError("Username taken")
        return username

# views.py
def signup(request):
    if request.method == 'POST':
        form = SignupForm(request.POST)
        if form.is_valid():
            User.objects.create_user(
                form.cleaned_data['username'],
                form.cleaned_data['email'],
                form.cleaned_data['password']
            )
            return redirect('home')
    else:
        form = SignupForm()

    return render(request, 'signup.html', {'form': form})

Benefits:

  • ✓ Validation centralized
  • ✓ Reusable
  • ✓ Testable
  • ✓ CSRF protection
  • ✓ Field-level error messages

Category 3: Architecture Smells

Smell #11: The Circular Import

What you see:

# models.py
from .utils import calculate_price  # Import from utils

class Product(models.Model):
    price = models.DecimalField(...)

# utils.py
from .models import Product  # Import from models -> CIRCULAR IMPORT!

def calculate_price(product_id):
    product = Product.objects.get(id=product_id)
    return product.price * 0.9

The smell: Files importing each other

Why it’s bad:

# Import error
ImportError: cannot import name 'Product' from partially initialized module

How to fix:

# Option 1: Move function to model
class Product(models.Model):
    def final_price(self):
        # Logic here (no import needed)
        pass

# Option 2: Late import
def calculate_price(product):
    from .models import Product  # Import inside function
    # Use Product
    pass

# Option 3: Move to separate service
# services/pricing.py (no model imports)
def calculate_price(price, discount):
    return price * (1 - discount)

# models.py
from services.pricing import calculate_price

class Product(models.Model):
    def final_price(self):
        return calculate_price(self.price, self.discount)

Detection:

  • Import errors on startup
  • Tests failing mysteriously

Smell #12: The Settings Soup

What you see:

# settings.py - 800 lines
# Random order
# No comments
# Mixed concerns

SECRET_KEY = '...'
DEBUG = True
STRIPE_KEY = '...'
ALLOWED_HOSTS = []
EMAIL_HOST = '...'
CELERY_BROKER = '...'
CUSTOM_SETTING_1 = '...'
LOGGING = {...}
# ... 700 more lines

The smell: Unmaintainable settings file

Why it’s bad:

  • Hard to find settings
  • Hard to change
  • Secrets mixed with config
  • No environment separation

How to fix:

# settings/
#   __init__.py
#   base.py        # Common settings
#   development.py # Dev overrides
#   production.py  # Prod overrides
#   test.py        # Test overrides

# settings/base.py
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

# Security
SECRET_KEY = os.environ.get('SECRET_KEY')

# Application
INSTALLED_APPS = [...]
MIDDLEWARE = [...]

# Database
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME'),
    }
}

# settings/development.py
from .base import *

DEBUG = True
ALLOWED_HOSTS = ['localhost']

# settings/production.py
from .base import *

DEBUG = False
ALLOWED_HOSTS = [os.environ.get('DOMAIN')]

Benefits:

  • ✓ Organized
  • ✓ Environment-specific
  • ✓ Secrets in environment
  • ✓ Easy to maintain

Smell #13: The Magic Number

What you see:

# Scattered throughout code
if user.age > 18:
    pass

if subscription_days < 30:
    pass

if file_size > 5242880:  # What is this?
    pass

if response.status_code == 402:  # Why 402?
    pass

The smell: Unexplained numbers in code

Why it’s bad:

  • Hard to understand
  • Hard to change (scattered everywhere)
  • Easy to make mistakes
  • No context

How to fix:

# settings.py or constants.py
MINIMUM_AGE = 18
TRIAL_PERIOD_DAYS = 30
MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024  # 5 MB
PAYMENT_REQUIRED_STATUS = 402

# In code
if user.age > MINIMUM_AGE:
    pass

if subscription_days < TRIAL_PERIOD_DAYS:
    pass

if file_size > MAX_FILE_SIZE_BYTES:
    pass

if response.status_code == PAYMENT_REQUIRED_STATUS:
    pass

Or use Enums:

from enum import IntEnum

class SubscriptionStatus(IntEnum):
    TRIAL = 1
    ACTIVE = 2
    CANCELLED = 3
    EXPIRED = 4

# Usage
if user.subscription == SubscriptionStatus.TRIAL:
    pass

Smell #14: The Global State

What you see:

# globals.py
current_user = None
current_request = None

# views.py
import globals

def view1(request):
    globals.current_user = request.user
    process_something()

def process_something():
    # Uses global state
    user = globals.current_user

The smell: Using global variables for state

Why it’s bad:

  • Race conditions (multi-threading)
  • Hard to test
  • Implicit dependencies
  • Unpredictable behavior

How to fix:

# Pass explicitly
def view1(request):
    process_something(request.user)

def process_something(user):
    # Explicit dependency
    pass

# Or use thread-local storage (Django does this)
from threading import local

_thread_locals = local()

def set_current_user(user):
    _thread_locals.user = user

def get_current_user():
    return getattr(_thread_locals, 'user', None)

Rule: Avoid global state. Pass data explicitly.

Smell #15: The God Object

What you see:

class Utils:
    @staticmethod
    def send_email():
        pass

    @staticmethod
    def calculate_tax():
        pass

    @staticmethod
    def format_date():
        pass

    @staticmethod
    def validate_phone():
        pass

    # 50 more unrelated methods

The smell: Class/module with unrelated functions

Why it’s bad:

  • No cohesion
  • Hard to find functions
  • Grows indefinitely
  • Tight coupling

How to fix:

# Separate by concern
# email_service.py
class EmailService:
    @staticmethod
    def send(to, subject, body):
        pass

# tax_calculator.py
class TaxCalculator:
    @staticmethod
    def calculate(amount, rate):
        pass

# formatters.py
class DateFormatter:
    @staticmethod
    def format(date, format_string):
        pass

# validators.py
class PhoneValidator:
    @staticmethod
    def validate(phone):
        pass

Rule: One class/module = One responsibility

Quick Smells (16–30)

Smell #16: The Print Debugger

print("Got here!")  # ✗ Use logging
print(user)  # ✗ Use debugger or logging

Smell #17: The Hardcoded Path

'/Users/john/projects/myapp/data.csv'  # ✗
# Use: BASE_DIR / 'data.csv'  ✓

Smell #18: The Timezone Naive

datetime.now()  # ✗ Timezone naive
datetime.now(timezone.utc)  # ✓ Timezone aware

Smell #19: The String Status

if order.status == "paid":  # ✗ Typo risk
# Use: if order.status == Order.Status.PAID  ✓

Smell #20: The Missing Migration

# Changed model but didn't run makemigrations
# ✗ Production breaks on deploy
# ✓ Always: python manage.py makemigrations

Smell #21: The Dead Code

def old_function():
    # Not used anywhere
    pass

# commented_out_code()  # 50 lines commented
# ✗ Delete it (it's in git)

Smell #22: The Long Line

user = User.objects.filter(is_active=True, email__icontains=search, created_at__gte=start_date, created_at__lte=end_date, subscription_status='paid')
# ✗ 150+ characters
# ✓ Break into multiple lines

Smell #23: The Premature Optimization

# Before measuring
class ComplexCachingSystem:
    # 200 lines of caching logic
    # Used for 10 queries/day
    # ✗ Overkill

Smell #24: The Secret Exposed

SECRET_KEY = 'django-insecure-hardcoded-key'  # ✗
# ✓ os.environ.get('SECRET_KEY')

Smell #25: The Missing Test

# Critical payment processing
def charge_card(user, amount):
    # 50 lines of logic
    # No tests ✗

Smell #26: The Try-Catch All

try:
    # Code
except:  # ✗ Catches everything, even KeyboardInterrupt!
    pass

# ✓ Be specific
except PaymentError:
    pass

Smell #27: The Mutation Surprise

def add_discount(products):
    for product in products:
        product.price *= 0.9  # ✗ Mutates input!
    return products

# ✓ Return new objects or be explicit

Smell #28: The Boolean Trap

send_email(user, True, False, True)  # ✗ What do these mean?

# ✓ Named parameters
send_email(
    user,
    send_welcome=True,
    send_notification=False,
    use_template=True
)

Smell #29: The Nested Nightmare

if user:
    if user.is_active:
        if user.subscription:
            if user.subscription.is_valid:
                if user.subscription.plan == 'premium':
                    # 5 levels deep ✗

Fix with guards:

if not user:
    return

if not user.is_active:
    return

if not user.subscription or not user.subscription.is_valid:
    return

if user.subscription.plan == 'premium':
    # Clean! ✓

Smell #30: The Import *

from django.db.models import *  # ✗ 
from utils import *  # ✗ Where do functions come from?

# ✓ Explicit imports
from django.db.models import Model, CharField, ForeignKey

The Code Review Checklist

When reviewing code (or your own), check for:

Database (5 smells)

  • Queries in loops (N+1)
  • Missing indexes
  • No cascade strategy
  • Fat models
  • Raw SQL when ORM works

Views (5 smells)

  • God views (>100 lines)
  • Logic in templates
  • Missing CSRF
  • Silent exceptions
  • Manual form validation

Architecture (5 smells)

  • Circular imports
  • Settings chaos
  • Magic numbers
  • Global state
  • God objects

Quick checks (15 smells)

  • Print statements
  • Hardcoded paths
  • Timezone naive
  • String statuses
  • Missing migrations
  • Dead code
  • Long lines (>120 chars)
  • Premature optimization
  • Exposed secrets
  • Missing tests
  • Catch-all exceptions
  • Mutation surprises
  • Boolean traps
  • Deep nesting (>3 levels)
  • Import *

The Smell Severity Guide

🔴 Critical (Fix immediately)

  • Missing CSRF
  • Exposed secrets
  • SQL injection risk
  • Silent exceptions

🟠 High (Fix before merge)

  • N+1 queries
  • Missing indexes
  • No cascade strategy
  • God views/models

🟡 Medium (Fix when refactoring)

  • Template logic
  • Magic numbers
  • Deep nesting
  • Long lines

🟢 Low (Nice to have)

  • Dead code
  • Print statements
  • Import *

Building Your Nose

Practice routine:

Week 1: Focus on 5 smells

  • Pick 5 from list
  • Review your code for them
  • Note when you find them

Week 2: Add 5 more

  • Previous 5 + 5 new
  • Total: 10 smells

Week 3: Pattern recognition

  • Review others’ code
  • Spot smells instantly
  • Explain why it’s a problem

Week 4: Automatic detection

  • Smells become obvious
  • Trust your intuition
  • Prevent before writing

After 1 month: You’ll smell problems before they become bugs.

Conclusion

The truth: Most bugs announce themselves before they happen.

The 30 smells covered:

Database: N+1, indexes, cascade, fat models, raw SQL Views: God view, template logic, CSRF, exceptions, forms Architecture: Circular imports, settings, magic numbers, globals, god objects Quick: 15 common patterns

How to use this:

  1. Learn patterns — Recognize the smells
  2. Understand why — Know the problems
  3. Know fixes — Have solutions ready
  4. Build intuition — Practice daily
  5. Trust your nose — If it smells, investigate

Remember:

  • Code that smells is trying to tell you something
  • Listen to your instincts
  • Refactor early, refactor often
  • Prevention > cure

Start today:

  1. Review your latest code
  2. Find 3 smells
  3. Fix them
  4. Prevent them tomorrow

Your nose will sharpen with practice.

Thanks for reading! ❤

If this helped you, consider clapping (50 👏 s), following, or sharing it. A writer without readers is just talking to themselves — so your time means everything.

Let’s keep building better, together.


메타데이터
post_id
87022c8332ec
slug
django-code-smells-the-30-patterns-that-signal-trouble-87022c8332ec
url
https://medium.com/@anas-issath/django-code-smells-the-30-patterns-that-signal-trouble-87022c8332ec
canonical_url
https://medium.com/@anas-issath/django-code-smells-the-30-patterns-that-signal-trouble-87022c8332ec
author_url
https://medium.com/@anas-issath
status
ok
fetched_at
2026-07-29 21:15:00