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

Table of contents:
clean and full_cleanDJANGO_.envmypy / type annotationsFew points to navigate yourself:
That's about it ✨
Hello 👋
This is the Django Styleguide, created by us, the folks at HackSoft.
Few important notes about it:
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:
You can also watch Radoslav Georgiev & Ivaylo Bachvarov's discussion on HackCast, around the Django Styleguide:
When it comes to the Django Styleguide, there are 3 general ways of using it:
We recommend point number 2:
The core of the Django Styleguide can be summarized as follows:
In Django, business logic should live in:
clean method for additional validations (with some exceptions).In Django, business logic should not live in:
save method.Model properties vs selectors:
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 put your business logic in APIs / Views / Serializers / Forms?
Relying on generic APIs / Views, with the combination of serializers & forms does 2 major things:
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:
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:
A, B, C, and D. Where do you put it?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:
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.
We recommend starting every new project with some kind of cookiecutter. Having the proper structure from the start pays off.
Few examples:
Styleguide-Example project as a starting point.cookiecutter-django since it has a ton of good stuff inside.Models should take care of the data model and not much else.
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
clean and full_cleanLets 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:
Validation should be moved to the service layer if:
It's OK to have validation both in
cleanand in the service, but we tend to move things in the service, if that's the case.
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.
$ claude mcp add Django-Styleguide \
-- python -m otcore.mcp_server <graph>