MCPcopy
hub / github.com/HackSoftware/Django-Styleguide

github.com/HackSoftware/Django-Styleguide @main sqlite

repository ↗ · DeepWiki ↗
5 symbols 13 edges 1 files 0 documented · 0%
README

Django Styleguide

👀 Need help with your Django project? HackSoft is here for you. Reach out at consulting@hacksoft.io

Django Styleguide

Table of contents:

How to ask a question or propose something?

Few points to navigate yourself:

  1. If you've read the Django Styleguide & you have questions or suggestions, the simplest thing you can is to open an issue. We will respond.
  2. Even if you have a question that you are not sure if it's related to the Django Styleguide - just open an issue anyway. We will respond.
  3. If you want to see a code example, make sure to head to the Django Styleguide Example repository. We treat this as a "Django test project", combining best practices & also examples from our blog.

That's about it ✨

What is this?

Hello 👋

This is the Django Styleguide, created by us, the folks at HackSoft.

Few important notes about it:

  1. It's derived from many years of experience & many Django projects, both big & small.
  2. It's pragmatic. All things mentioned here are things tested in production.
  3. It's opinionated. This is how we build applications with Django.
  4. It's not the only way. There are other ways of building & structuring Django projects that can do the job for you.
  5. We have a Django-Styleguide-Example to show most of the styleguide in an actual project.

You can watch Radoslav Georgiev's Django structure for scale and longevity for the philosophy behind the styleguide:

Django structure for scale and longevity by Radoslav Georgiev

You can also watch Radoslav Georgiev & Ivaylo Bachvarov's discussion on HackCast, around the Django Styleguide:

HackCast S02E08 - Django Community & Django Styleguide

How to use it?

When it comes to the Django Styleguide, there are 3 general ways of using it:

  1. Strictly follow everything written here.
  2. Cherry-pick whatever makes sense to you, based on your specific context.
  3. Don't follow anything written here.

We recommend point number 2:

  • Read the styleguide.
  • Decide what's going to work best for you.
  • Adapt for your specific case.

Overview

The core of the Django Styleguide can be summarized as follows:

In Django, business logic should live in:

  • Services - functions, that mostly take care of writing things to the database.
  • Selectors - functions, that mostly take care of fetching things from the database.
  • Model properties (with some exceptions).
  • Model clean method for additional validations (with some exceptions).

In Django, business logic should not live in:

  • APIs and Views.
  • Serializers and Forms.
  • Form tags.
  • Model save method.
  • Custom managers or querysets.
  • Signals.

Model properties vs selectors:

  • If the property spans multiple relations, it should better be a selector.
  • If the property is non-trivial & can easily cause N + 1 queries problem, when serialized, it should better be a selector.

The general idea is to "separate concerns" so those concerns can be maintainable / testable.

Why not?

🤔 Why not put your business logic in APIs / Views / Serializers / Forms?

Relying on generic APIs / Views, with the combination of serializers & forms does 2 major things:

  1. Fragments the business logic in multiple places, making it really hard to trace the data flow.
  2. Hides things from you. In order to change something, you need to know the inner-workings of the abstraction that you are using.

Generic APIs & Views, in combination with serializers & forms, is really great for the straightforward "CRUD for a model" case.

From our experience, so far, this straightforward case rarely happens. And once you leave the happy CRUD path, things start to get messy.

And once things start to get messy, you need more "boxes", to organize your code in a better way.

This styleguide aims to:

  1. Give you those "boxes".
  2. Help you figure out your own "boxes", for your own specific context & needs.

Additionally, thanks to this comment - https://github.com/HackSoftware/Django-Styleguide/issues/170 - there's one additional way of looking at this:

The way your app should behave (or as we call it - "business logic") should not be related to the way you interface with it (be it an API, a management command or something else) and this is a very clear line, where we want to separate our concerns.

Of course, there are cases, where things can get intertwined, yet, a good baseline for thinking about this separation is - "core" vs. "interface".


🤔 Why not put your business logic in custom managers and/or querysets?

This is actually a good idea & you might introduce custom managers & querysets, that can expose better API, tailored to your domain.

But trying to place all of your business logic in a custom manager is not a great idea, because of the following:

  1. Business logic has its own domain, which is not always directly mapped to your data model (models)
  2. Business logic most often spans across multiple models, so it's really hard to choose where to place something.
  3. Let's say you have a custom piece of logic that touches models A, B, C, and D. Where do you put it?
  4. There can be additional calls to 3rd party systems. You don't want those in your custom manager methods.

The idea is to let your domain live separately from your data model & API layer.

If we take the idea of having custom queryset/managers and combine that with the idea of letting the domain live separately, we'll end up with what we call a "service layer".

Services can be functions, classes, modules, or whatever makes sense for your particular case.

With all that in mind, custom managers & querysets are very powerful tools and should be used to expose better interfaces for your models.


🤔 Why not put your business logic in signals?

From all of the available options, perhaps, this one will lead you to a very bad place very quickly:

  1. Signals are a great tool for connecting things that should not know about each other, yet, you want them to be connected.
  2. Signals are also a great tool for handling cache invalidation outside your business logic layer.
  3. If we start using signals for things that are heavily connected, we are just making the connection more implicit and making it harder to trace the data flow.

That's why we recommend using signals for very particular use cases, but generally, we don't recommend using them for structuring the domain / business layer.

Cookie Cutter

We recommend starting every new project with some kind of cookiecutter. Having the proper structure from the start pays off.

Few examples:

Models

Models should take care of the data model and not much else.

Base model

It's a good idea to define a BaseModel, that you can inherit.

Usually, fields like created_at and updated_at are perfect candidates to go into a BaseModel.

Here's an example BaseModel:

from django.db import models
from django.utils import timezone


class BaseModel(models.Model):
    created_at = models.DateTimeField(db_index=True, default=timezone.now)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True

Then, whenever you need a new model, just inherit BaseModel:

class SomeModel(BaseModel):
    pass

Validation - clean and full_clean

Lets take a look at an example model:

class Course(BaseModel):
    name = models.CharField(unique=True, max_length=255)

    start_date = models.DateField()
    end_date = models.DateField()

    def clean(self):
        if self.start_date >= self.end_date:
            raise ValidationError("End date cannot be before start date")

We are defining the model's clean method, because we want to make sure we get good data in our database.

Now, in order for the clean method to be called, someone must call full_clean on an instance of our model, before saving.

Our recommendation is to do that in the service, right before calling save:

def course_create(*, name: str, start_date: date, end_date: date) -> Course:
    obj = Course(name=name, start_date=start_date, end_date=end_date)

    obj.full_clean()
    obj.save()

    return obj

This also plays well with Django admin, because the forms used there will trigger full_clean on the instance.

We have few general rules of thumb for when to add validation in the model's clean method:

  1. If we are validating based on multiple, non-relational fields, of the model.
  2. If the validation itself is simple enough.

Validation should be moved to the service layer if:

  1. The validation logic is more complex.
  2. Spanning relations & fetching additional data is required.

It's OK to have validation both in clean and in the service, but we tend to move things in the service, if that's the case.

Validation - constraints

As proposed in this issue, if you can do validation using Django's constraints, then you should aim for that.

Less code to write, less code to maintain, the database will take care of the data even if it's being inserted from a different place.

Lets look at an example!

class Course(BaseModel):
    name = models.CharField(unique=True, max_length=255)

    start_date = models.DateField()
    end_date = models.DateField()

    class Meta:
        constraints = [
            models.CheckConstraint(
                name="start_date_before_end_date",
                check=Q(start_date__lt=F("end_date"))
            )
        ]

Now, if we try to create new object via course.save() or via Course.objects.create(...), we are going to get an IntegrityError, rather than a ValidationError.

This can actually be a downside (_this is not the case, starting from Django 4.1. Check the extra section below.

Core symbols most depended-on inside this repo

get_new_toc
called by 1
tools/update_toc.py
get_readme
called by 1
tools/update_toc.py
save_readme
called by 1
tools/update_toc.py
replace_toc
called by 1
tools/update_toc.py
main
called by 1
tools/update_toc.py

Shape

Function 5

Languages

Python100%

Modules by API surface

tools/update_toc.py5 symbols

For agents

$ claude mcp add Django-Styleguide \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact