Split and Organize Your Django Models Files for Better Project Structure

Simplify your Django projects by breaking down large models.py files into smaller, manageable pieces within a Python package.

Posted by Pierre on Feb. 17, 2024, 4:03 p.m.

Over time, the models.py files in a Django project can become large and difficult to manage, especially if they contain a lot of logic. It is generally beneficial to avoid having “fat” models in a Django project (a topic that could merit its own dedicated article). However, it’s quite straightforward (and almost effortless) to divide a large models.py file into several smaller files within a Python package.

This simple tips can help you to clean (a little bit) your codebase!

The example

Let’s say that you have a blog app in your Django project. Its structure looks like this:

.
├── blog
|    ├── __init__.py
|    ├── models.py
|    ├── tests.py
|    ├── views.py

where the models.py file contains the following models:

from django.contrib.auth import get_user_model
from django.db import models

User = get_user_model()

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()

    author = models.ForeignKey(User)

class Comment(models.Model):
    article = models.ForeignKey(Article)
    content = models.TextField()

    author = models.ForeignKey(User)

Using strings to reference models in Django relationship fields

The first thing to do is to use the name of your model, instead of the actual model, in your relationship fields (such as ForeignKey, ManyToManyField, etc …). This will reduce the risk for you to faces a circular import while creating the Python package. So we modify the declaration of the Comment.article field like this:

from django.contrib.auth import get_user_model
from django.db import models

User = get_user_model()

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()

    author = models.ForeignKey(User)

class Comment(models.Model):
    article = models.ForeignKey("Article")  # Reference the name of the model instead of the model itself.
    content = models.TextField()

    author = models.ForeignKey(User)

Please note that:

  • Referencing User is fine because it came from Django itself and it is fine for our models to depends on Django.
  • If you need to reference a model from another app, your can do it by prefixing its name with the name of the app (as an example: blog.Article).

Transforming your models file into a Python package

Now you can create a models python package within your Django app:

.
├── blog
|    ├── __init__.py
|    ├── models
|    |   ├── __init__.py
|    |   ├── article.py
|    |   ├── comment.py
|    ├── models.py
|    ├── tests.py
|    ├── views.py

Copy paste your models into the new files:

# blog/models/article.py

from django.contrib.auth import get_user_model
from django.db import models

User = get_user_model()

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()

    author = models.ForeignKey(User)
# blog/models/comment.py

from django.contrib.auth import get_user_model
from django.db import models

User = get_user_model()

class Comment(models.Model):
    article = models.ForeignKey("Article")  # Reference the name of the model instead of the model itself.
    content = models.TextField()

    author = models.ForeignKey(User)

and delete the models.py file!

At this point, if you run the python manage.py check command, Django will warn you that it cannot locate your models any more..

All we have to do to finalize our new models Python package, and allow Django to discover our models, is to initialize it using its __init__.py file:

# blog/models/__init__.py

from .article import Article
from .comment import Comment

__all__ = ["Article", "Comment"]

In Python, __all__ is a list that specifies the public objects within a given module. A module can be either a Python file or a Python package. When the statement from <module> import * is used, only the items listed in the __all__ attribute will be imported from the module.

  • It allows Django to discover your models the same way as with a regular models.py file.
  • Using the statement from blog.models import Article remains possible, ensuring compatibility with Django.
  • You can also write from blog.models.article import Article. In Python, privacy does not exist in its absolute sense. Every object or attribute can be accessed using introspection techniques.

Conclusion

I hope you’ve learned something today. I consistently apply this organizational approach for the models file in my Django apps, regardless of whether they are new or existing projects. This is something that you can easily reproduce on other resources such as:

  • forms
  • model admin classes
  • views
  • and so on

For test organization, you can follow a similar pattern by grouping tests related to each model in separate files, such as test_article.py. This makes it easy to find and run the appropriate tests for each model.