As the old programming adage goes: Show me your algorithm, and I will remain puzzled but show me your data structure, and I will be enlightened.

An application’s data structure should be your primary focus when designing a Django application. A well-designed data model allows for a smooth development process and a source code that is easy to understand and maintain.

But how do you make sure that your Django models are in the best shape? By following some battle-tested Django models best practices, that’s how.

Here are 6 tips to help you get better at building Django models.

1. Make them thick

If your background isn’t in object-oriented languages, you may think of models as nothing else than data containers. That means you’ll be storing your business logic in view. For the simplest example, let’s assume that we store client data in a model:

1
2
3
4
5
class Client(models.Model):
   """Our company's client"""
   title=models.CharField(max_length=5)
   first_name=models.CharField(max_length=128)
   last_name=models.CharField(max_length=128)

Now you may be building a Django web application where you need to generate the “full address” of your client (like “Rev. John Smith”). Using the full format call in various places would be a clear violation of the DRY (don’t repeat yourself) principle.

So instead you can try to create a utility function that would format the full address for you. That’s going to introduce yet another layer into your application architecture. The proper way of handling this is by adding a property to your model: 

1
2
3
4
@property
def full_address(self):
   """The full way of addressing a client"""
   return '{} {} {}'.format(title, first_name, last_name)

As a general rule, all the business logic that isn’t related to handling requests, raw data processing, and frontend-dependent presentation fits best into the model.

2. Don’t be afraid to override

Sometimes the business logic you want to implement may not be related to model instances, but include information about how the entire querysets behave.

A simple example of this is a soft-deletion mechanism. Let’s assume that you don’t want to remove your objects permanently, but only mark them as deleted for most cases. We can do that by running the following command:

1
2
3
4
class Entry(models.Model):
   """An entry for our blog"""
   content=models.TextField()
   deleted_ts=models.DateTimeField(null=True)

Now, anytime you want to retrieve entries that aren’t deleted, you need to filter them by deleted_ts__isnull=False which certainly goes against the DRY principle. Just imagine how much code you’d have to change if you wanted to introduce your soft-deletion logic into an existing project or refactor it somehow!

It also opens the possibility that you forget this filter and display something that wasn’t supposed to be displayed. A better way to manage it is by overriding the default manager of your model:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class SoftDeletableQS(models.QuerySet):
   """A queryset that allows soft-delete on its objects""" 

   def delete(self, **kwargs):
       self.update(deleted_ts=timezone.now(), **kwargs)

   def hard_delete(self, **kwargs):
       super().delete(**kwargs)

class SoftDeletableManager(models.Manager):
   """Manager that filters out soft-deleted objects"""
   def get_queryset(self):
       return SoftDeletableQS(
           model=self.model, using=self._db, hints=self._hints
       ).filter(
           deleted_ts__isnull=True
       )

class Entry(models.Model):
   """An entry for our blog"""
   objects = SoftDeletableManager()
   archive_objects = models.Manager()
   content=models.TextField()
   deleted_ts=models.DateTimeField(null=True)
   
   def delete(self):
       """Softly delete the entry"""
       self.deleted_ts = timezone.now()
       self.save()
   
   def hard_delete(self):
       """Remove the entry from the database permanently"""
       super().delete()

The above might seem a little intimidating at first, but the logic is very simple.

We replace the default object attribute, affecting how the object manager is accessed. We also override the delete method on both our model and a queryset, so that calling it would only mark objects as deleted. Lastly, we make our manager filter every queryset it returns.

Now, if we want to access soft-deleted objects or do a hard delete, we must do so explicitly by using archive_objects manager and hard_delete method. There is no risk now that anyone accidentally removes an object permanently or displays an entry that was marked as deleted.

3. Know your inheritances

When using thick models, you need to understand how model inheritance works in Django ORM. While we can always use the full power of Python’s object system, there’s a difference in how your model hierarchy is reflected in the database.

There are three possible ways to do it, each one with its own applications.

a) Using abstract models

Abstract models are models that have no reflection in the database and can’t be instantiated. You should use abstract models if you want to share logic between models. For example, the previous concept of a self-deletable model could be abstracted into this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SoftDeletableModel(models.Model):
   """An abstract parent class for soft-deletable models"""
   objects = SoftDeletableManager()
   archive_objects = models.Manager() 

   deleted_ts=models.DateTimeField(null=True)

   class Meta:
       abstract=True
   
   def delete(self):
       """Softly delete the object"""
       self.deleted_ts = timezone.now()
       self.save()
   
   def hard_delete(self):
       """Remove the object permanently from the database"""
       super().delete()

The only difference with the previous Entry model is that here we specify only the deleted_ts field here. We also mark the class as abstract. That way Django isn’t going to create a table for it and would forbid us from instantiating it directly. We can now subclass the class as many times as we want. The classes would share the logic defined here, but not the database structure.

b) Multi-table inheritance

If you want to share the database structure, you can use the multi-table inheritance. That way Django creates separate tables for your superclass and subclass, and binds them with a one-to-one relation.

Let’s imagine that some of your clients have a VIP status that entitles them to a special treatment that includes having a dedicated employee taking care of their business. The definition could look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Client(models.Model):
   """Our company's client"""
   first_name=models.CharField(max_length=128)
   last_name=models.CharField(max_length=128) 

class VIPClient(models.Model):
   """Client with VIP status"""
   
   manager=ForeignKey(get_user_model())
   
   def render_postcard(self, request):
      """Create a HTML postcard to display for the VIP user"""
      render(request, 'postcard.html', {
          'user': self, 'manager': self.manager
      )

With this design, if you retrieve Client objects you would get all the clients, both regular and VIP. Retrieving VIPClient objects would yield only the VIP ones with all the business logic.

Note: Never use multi-table inheritance if you don’t intend to use querysets on superclass. That’s because the unnecessary JOINs would affect your database performance. For instance, omitting the abstract attribute in the example above would create a table shared by all soft-deletable models that would be JOIN-ed in every request.

c) Proxy models

If you want to add some business logic to an existing model without affecting its database schema, use proxy models. Our self-deletion mechanism can now be complete:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Entry(SoftDeletableModel):
   """A blog entry"""
   text=models.TextField() 

class ArchivedEntry(Entry):
   """Archive blog entries - hard-deletable"""
   objects = models.Manager()

class Meta:
       proxy=True

def delete(self):
         """Permanently delete the entry"""
       super().hard_delete()

We can now use Entry and ArchivedEntry models as if they were separate models. The latter behaves here like a hard-deletable and allows us to view soft-deleted objects.

4. Use proper field types

It might be tempting to learn and use just a handful of field types available in Django. CharField, DateTimeField, IntegerField, and FloatField should be enough for everyone, after all.

But you’ll soon find yourself either dealing with tons of validation bugs or reinventing the wheel by writing complex validation logic.

Why not check whether there exists a field type dedicated to your needs? That way you’ll be using the most specific type that encompasses possible values you use. It’s also smart to configure your type to be as restrictive as possible.

Here are a few more specific rules for dealing with field types in Django development:

  1. Use DecimalField for currencies and for all tasks where human readability is more important than computation speed.
  2. Use PositiveIntegerField anytime negative values don’t make any sense for your attribute.
  3. Set null=False if you don’t plan to have missing data in a column. The exception being textual fields where Django, unfortunately, stores empty strings for missing data. Using non-nullable fields not only causes your data to be validated, but also facilitates optimization in most database engines.
  4. Limit the choices of the field if that makes sense. You can use choices with any field type.

5. Where to put the validation?

You might be a little confused about where to put the validation. Django allows validating both on models and on forms. Not to mention the possibility of validation offered by Django Rest Framework and other third-party frameworks.

Here’s one rule to follow:

If the validation isn’t specific to the raw data or a particular use-case, it should go into your models. That way you get a DRY and solid data model that ensures you’re only working on valid data in all use-cases.

6. Proper attention to detail

Because Django models form the base of your application, attention to detail pays off when creating its other layers.

These details include:

  1. Proper names for your models, fields, and methods. When choosing names, imagine that you’re seeing the model for the first time. Can you guess easily what is stored under this field or what this model represents?
  2. Good docstrings that describe the behavior and usage of your models.
  3. Define the __str__ method for every model. It should render a string that allows identifying what this model implements easily. That applies to models handling relations as well. A string representation for them might look like “Entry ‘Lorem ipsum’ is tagged as #important”. The __str__ is used not only in the shell but also by the Django admin by default, so your debugging and administration work could really benefit from taking a moment for this step.
  4. Having internationalized verbose_name and help_text for your fields will help you in a more DRY user interface. It would naturally propagate to your forms, REST APIs, etc.

Build better Django models

Building a good data model in Django is never a waste of your time. On the contrary, having a clear and solid model makes designing the rest of the application in your Django project a breeze.

So don’t be shy to take some extra time to plan your model structure. Consult it with your teammates. Make sure that it’s easy to understand for everyone and captures your application’s needs. You’ll thank yourself in the future.

Do you have any questions about Django models and other Django best practices? Please share them in the comments section; I’m happy to share more insights with you.

And if you’re looking to hire Django developers, get in touch with us – our Django team has realized many projects for different industries

Szymon Pyzalski
Szymon
Backend Engineer

Szymon is a backend developer at Sunscrapers. He’s an enthusiastic Pythonista with the main area of expertise in Django. After hours, you can find him deep in thought in front of a Go board.

Python Web development

9 productivity tips for software developers: touch typing, dotfiles, and more

The keyboard and the mouse are the most common devices that interface us with computers. We don’t have brain-computer interfaces just yet, and we need to rely on them [...]

Python

14 Python resources for intermediate and advanced Python developers

Python developers can choose from a great variety of learning resources. But sifting through all the books, tutorials, and courses can be difficult if you’re looking for something particular. [...]

Join our newsletter.

Scroll to bottom

Hi there, we use cookies to provide you with an amazing experience on our site. If you continue without changing the settings, we'll assume that you're happy to receive all cookies on the Sunscrapers website. You can change your cookie settings at any time.

Learn more