Bad Part in Django

I was recently assigned to develop a web application using Django as the backend. During the development process, I came across some design aspects in Django that were less than ideal.

The Mysterious get_FOO_display()

In Django's Models, when you create a Choices field, let's call it FOO, Django automatically generates a corresponding function called get_FOO_display(). Developers can use this function to retrieve the "human-readable value" for that field.

The design is quite strange because get_FOO_display() seems to appear out of nowhere. Although it depends on the existence of the FOO field, there is no explicit static relationship connecting them.

from django.db import models

class Person(models.Model):
    SHIRT_SIZES = [
        ("S", "Small"),
        ("M", "Medium"),
        ("L", "Large"),
    ]
    name = models.CharField(max_length=60)
    shirt_size = models.CharField(max_length=2, choices=SHIRT_SIZES)
    
>>> p = Person(name="Fred Flintstone", shirt_size="L")
>>> p.save()
>>> p.shirt_size
'L'
>>> p.get_shirt_size_display()
'Large'

If you decide to change the name of the SHIRT_SIZES field or remove it, Python, being a dynamic language, will not provide a warning indicating that you are using an invalid function. Everything appears to be fine until it crashes when the code reaches get_shirt_size_display().

String-Bound related_name

When defining a foreign key field in Django's Models, it is necessary to include a related_name attribute. This attribute allows you to locate the object from the referenced foreign key. However, it is important to note that this attribute can only be set as a string, which means that there are no constraints on this binding relationship.

class Asset(models.Model):
    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        related_name='assets',
        on_delete=models.CASCADE
    )

If you refactor the code and change the value of related_name, you won't get any warnings, but incorrect code has already been produced. If there are still unchanged instances accessing similar code in your project, it could lead to a crash.

# You can access Asset object from owner like below:
user = self.context['request'].user
user_assets = user.assets

If this code appears frequently in your project, it will raise the cost of refactoring and make the code more difficult to maintain.

Field Queries

Django's field query syntax is user-friendly but also carries risks. For example, you can use the following syntax to query objects with a pub_date earlier than "2006-01-01".

>>> Entry.objects.filter(pub_date__lte="2006-01-01")

Django achieves this by utilizing Python's Keyword Arguments feature. However, due to its dynamic implementation, attributes like pub_date__lte do not provide any binding constraints. As the project grows, the cost of refactoring related fields becomes increasingly expensive.

Challenges of Providing Binding Constraints in Dynamic Languages

The issues mentioned above may not necessarily be due to Django's shortcomings, but rather a result of using Python, a dynamic language. Dynamic languages lack the compile-time constraints found in static languages, which can lead to potential errors and increased costs for refactoring.

Dynamic languages offer convenience in development by eliminating the need for code compilation and saving time on larger projects. However, this convenience comes at the expense of lacking the binding constraints provided by static languages during compile-time. Errors in the code may go unnoticed until they are executed.

At the opposite end of the spectrum are statically typed languages such as Rust. Rust ensures strict type checks and memory safety, resulting in faster execution without the need for garbage collection. However, this also means that compilation times in Rust can be longer compared to most other statically typed languages.