django-antipatterns

repository·master·Indexed 42 days ago

https://github.com/hapytex/django-antipatterns

A knowledge base documenting common mistakes and anti-patterns in Django development, providing detailed explanations and actionable mitigation strategies to resolve or avoid these issues.

Tokens
31.3K
Snippets
92
Records
114
Agent score
52%

What's inside django-antipatterns

  1. Overview of django-antipatterns

    master

    django-antipatterns is a repository dedicated to documenting common anti-patterns found in Django projects. Each entry provides a detailed explanation of the specific problem, the reasoning behind why it is considered an anti-pattern, and actionable mitigation strategies to resolve or avoid the issue.

    You can view the rendered documentation online at django-antipatterns.com.

  2. Difference between ForeignKey and OneToOneField

    master

    In Django models, choosing between ForeignKey and OneToOneField depends on the cardinality of the relationship you want to enforce.

    Key Differences

    • ForeignKey (Many-to-One): Multiple records in the child model can refer to the same record in the parent model. By default, the reverse relationship uses a RelatedManager (e.g., user.profile_set), allowing you to call .all() to retrieve a queryset of related objects.
    • OneToOneField (One-to-One): A subclass of ForeignKey that automatically sets unique=True. This ensures that a parent record can be associated with at most one child record. The reverse relationship defaults to the model name itself (e.g., user.profile) rather than a _set manager.

    When using a OneToOneField, accessing the reverse relation returns the object directly if it exists. If no related object exists, Django raises a RelatedObjectDoesNotExist exception.

    FeatureForeignKeyOneToOneField
    Relation Typemany-to-oneoptional-to-one
    UniquenessNot unique (unless unique=True is set)Always unique
    Default related_namemodelname_setmodelname
    Reverse AccessReturns a RelatedManagerReturns the object (or raises error)
    Extra ParametersN/Aparent_link

    Important Nuances

    • Uniqueness Warning: If you define a ForeignKey with unique=True, Django will issue a warning suggesting you use OneToOneField instead.
    • Cardinality Misconception: A OneToOneField does not guarantee that a parent has exactly one child. It guarantees that a child has exactly one parent (if null=False) and a parent has at most one child. It does not automatically instantiate the child object when the parent is created.
  3. Avoid manually constructing slugs

    master

    Manually constructing slugs using simple string methods like .replace(' ', '-') is an antipattern. This approach often fails to handle special characters (like $ or .), diacritics (like é), or leading/trailing whitespace, resulting in invalid URLs or 'ugly' slugs that do not match the expected <slug:..> path converter behavior.

    Problems with manual construction:

    • Invalid characters: Characters like $ or . might be included, which are not accepted by Django's <slug:..> path converter.
    • Diacritic loss: Custom algorithms might strip characters like é entirely (e.g., turning café into caf) instead of normalizing them (e.g., cafe).
    • Formatting issues: Leading or trailing whitespace can result in slugs with leading or trailing hyphens (e.g., --to-be-or-not-to-be).
    # ANTIPATTERN: Manual construction in a view
    model_object.slug = model_object.title.replace(' ', '-')
  4. Avoid using HTML comments in Django templates

    master
    Using standard HTML comments (<!-- comment -->) in Django templates is an antipattern because the Django template engine renders them into the final HTML sent to the client. This can expose developer notes, internal logic, or server details to end-users. Instead, use Django's built-in comment syntax which ensures the content is stripped out during template rendering.
  5. Avoid using `request.POST or None` to initialize forms

    master

    In Django views, using form = MyForm(request.POST or None) is an antipattern. This pattern attempts to shorten code by handling both GET and POST requests in one line, but it relies on the truthiness of request.POST.

    If a POST request is sent with an empty payload (an empty QueryDict), request.POST evaluates to False. The or operator then selects None, resulting in an unbound form. An unbound form will cause form.is_valid() to return False, even if the empty request was technically valid according to the form's requirements.

    # ANTIPATTERN
    def someview(request):
        form = MyForm(request.POST or None)
        # form will be unbound if request.POST is empty, even on a POST request
  6. Implement a `SET_WITH(...)` deletion handler with object context

    master

    Standard Django SET(...) handlers accept a value or a callable that takes no arguments. However, you may need a handler that receives the specific model instance being processed to make a decision (e.g., choosing a new related object based on the instance's current state).

    To implement this, you can create a SET_WITH(func) wrapper. The provided func must accept the model instance as its only argument and return either:

    1. A new value for the field (e.g., a new related object).
    2. The special DO_CASCADE sentinel object to signal that the instance itself should be deleted (cascaded).

    This pattern allows for complex conditional logic during deletion, such as promoting a member to an organizer or deleting a meeting if no members remain.

    # app_name/deletion.py
    from django.db.models.deletion import CASCADE
    
    DO_CASCADE = object()
    
    def SET_WITH(func):
        def set_on_delete(collector, field, sub_objs, using):
            cascades = []
            for obj in sub_objs:
                result = func(obj)
                if result is DO_CASCADE:
                    cascades.append(obj)
                else:
                    collector.add_field_update(field, result, [obj])
            if cascades:
                CASCADE(collector, field, cascades, using)
        set_on_delete.deconstruct = lambda: ('app_name.SET_WITH', (func,), {})
        return set_on_delete
  7. How Django models.Manager and custom managers work

    master

    In Django, when you call methods like .all() or .get() on a manager (e.g., Post.objects.all()), the manager does not actually implement these methods itself. Instead, it dynamically delegates them to the QuerySet returned by the manager's get_queryset() method.

    Method Delegation

    Django uses inspection to find all public methods on the QuerySet class. It then creates wrapper methods on the Manager that follow this pattern:

    1. Call self.get_queryset() to get the base QuerySet.
    2. Call the requested method (like .all()) on that QuerySet.

    This allows custom managers to override get_queryset() to filter results automatically (e.g., a PublishedManager that only returns posts with status=PUBLISHED) while still providing access to all standard QuerySet API methods.

    Model Association

    A manager is often instantiated before the model it belongs to is fully defined. Django handles this via the contribute_to_class method. When a manager is attached to a model class, Django:

    1. Injects the model into the manager instance.
    2. Replaces the manager attribute on the model class with a ManagerDescriptor.

    Access Restrictions

    The ManagerDescriptor enforces two important Django behaviors:

    • Class-level access only: Managers can only be accessed via the Model class (e.g., Book.objects), not via model instances (e.g., my_book.objects will raise an AttributeError).
    • Abstract Model protection: Managers are not available on abstract models.
    • Swapped Model protection: Managers are not available if the model has been swapped for another model.
    class PublishedManager(models.Manager):
        def get_queryset(self):
            # This custom logic is what .all(), .filter(), etc. will use
            return super().get_queryset().filter(status=Post.Status.PUBLISHED)
    
    class Post(models.Model):
        status = models.CharField(max_length=10)
        
        objects = models.Manager()      # Default manager
        published = PublishedManager() # Custom manager
    
    # Usage:
    # Post.published.all() will now only return published posts
  8. Avoid using `_id` suffix for ForeignKey or OneToOneField names

    master

    In Django models, do not manually add an _id suffix to the name of a ForeignKey or OneToOneField.

    Django automatically creates an implicit 'twin' field with an _id suffix to store the actual primary key value. If you name your field other_model_id, Django's internal twin field becomes other_model_id_id, which is redundant and leads to confusing database column names.

    Instead, name the field after the related object itself.

    # BAD: Redundant suffix
    class MyModel(models.Model):
        other_model_id = models.ForeignKey(OtherModel, on_delete=models.CASCADE)
    
    # GOOD: Let Django handle the ID suffix
    class MyModel(models.Model):
        other_model = models.ForeignKey(OtherModel, on_delete=models.CASCADE)
  9. Avoid rendering content directly after a successful POST request

    master

    Rendering a template directly after a successful POST request is an antipattern. If a user refreshes their browser after seeing the rendered content, the browser will attempt to re-submit the POST request. This can lead to unintended side effects, such as duplicate database entries, multiple purchases, or duplicate comments.

    Problematic Pattern: Returning render() inside a successful if form.is_valid(): block after a POST request.

    from django.shortcuts import render
    
    def my_view(request):
        if request.method == 'POST':
            form = MyForm(request.POST, request.FILES)
            if form.is_valid():
                form.save()
                # ANTIPATTERN: Rendering directly after POST
                return render(request, 'some_success_template.html')
        else:
            form = MyForm()
        return render(request, 'some_template.html', {'form': form})
  10. Avoid giving `related_name` the same name as the relation

    master

    In Django models, the related_name parameter on a ForeignKey defines the name used to access the related objects in reverse (from the target model back to the source model).

    An antipattern occurs when related_name is set to the same name as the field itself (e.g., setting related_name='author' on a field named author). This is confusing because when accessing the relation from the target model, the name implies the target object (the Author) rather than the collection of related objects (the Posts).

    Example of the antipattern:

    class Profile(models.Model):
        pass
    
    class Post(models.Model):
        author = models.ForeignKey(
            Profile,
            on_delete=models.CASCADE,
            related_name='author'
        )
    
    # Confusing usage: accessing posts via the 'author' name
    my_profile.author.all()  # Returns a QuerySet of Posts, not an Author
    class Post(models.Model):
        author = models.ForeignKey(
            Profile,
            on_delete=models.CASCADE,
            related_name='author'
        )
    
    # QuerySet of related Post objects
    my_profile.author.all()
  11. Avoid passing function references to reverse()

    master

    In Django, the reverse() function allows you to pass either a view name string or a direct reference to a view function. However, passing the function reference itself (e.g., reverse(some_view)) is considered an antipattern because it can lead to resolution failures when using URL namespacing.

    When an app uses an app_name (namespacing), the parent URLResolver's .reverse_dict does not always contain entries for view function references defined within the included sub-URLs. This makes the URL impossible to reverse from the parent context. This issue is particularly problematic in independent Django apps where the consumer might include the app's URLs with a namespace, breaking the function-reference lookup.

    # ANTIPATTERN: Passing the function reference directly
    from app_name.views import some_view
    url = reverse(some_view)
  12. Avoid using plural names for Django model classes

    master

    Naming Django model classes with plural nouns (e.g., Cars) is considered an antipattern because Django uses the class name to automatically generate metadata like verbose_name and verbose_name_plural.

    Problems caused by plural names:

    • Incorrect Metadata: Django's automatic pluralization often results in grammatically incorrect strings (e.g., Cars._meta.verbose_name_plural becomes 'carss'). This affects the Django Admin and other packages that use these fields for forms and templates.
    • Grammatically Incorrect Queries: Querysets like Cars.objects.all() read as "All the cars objects", which is redundant.
    • Ugly Related Managers: Related model lookups become less elegant, such as ford.cars_set.all() instead of the preferred ford.car_set.all().

    How to resolve:

    Use a singular name for the model class. If you must match an existing database table name that is plural, use the db_table option to decouple the class name from the table name.

    # Avoid this:
    class Cars(models.Model):
        pass
    
    # Do this:
    class Car(models.Model):
        pass
    
    # If you need to match a plural database table:
    class Car(models.Model):
        class Meta:
            db_table = 'cars'