Design Principles in Python/Django

denisa_dev
Django Unleashed
Published in
4 min readMay 24, 2024

Design principles in Python, like in any programming language, help create clean, maintainable, and efficient code. Here are some key design principles with examples:

1. DRY (Don’t Repeat Yourself)

Avoid duplication of code by abstracting repeated patterns into functions or classes.

Django inherently promotes the DRY principle through its ORM, forms, and admin interface.

Example: Using Serializers

Instead of writing validation logic separately, DRF serializers allow you to encapsulate this logic within the serializer itself.


# serializers.py
from rest_framework import serializers
from .models import Expense, Category, Balance

class ExpenseSerializer(serializers.ModelSerializer):
class Meta:
model = Expense
fields = ['id', 'user', 'category', 'amount', 'description', 'date']

class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ['id', 'name', 'description', 'user']

class BalanceSerializer(serializers.ModelSerializer):
class Meta:
model = Balance
fields = ['id', 'total_balance']

2. KISS (Keep It Simple, Stupid)

Keep your code as simple as possible. Avoid unnecessary complexity.

Django’s design philosophy emphasizes simplicity and readability.

Example: Using Django Rest Framework Generic Views

Instead of writing complex view logic, you can use DRF’s generic views to handle common patterns like creating, retrieving, and listing objects.

# views.py
from rest_framework import generics
from .models import Expense, Category, Balance
from .serializers import ExpenseSerializer, CategorySerializer, BalanceSerializer

class ExpenseListView(generics.ListCreateAPIView):
queryset = Expense.objects.all()
serializer_class = ExpenseSerializer

class ExpenseDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Expense.objects.all()
serializer_class = ExpenseSerializer

class CategoryListView(generics.ListCreateAPIView):
queryset = Category.objects.all()
serializer_class = CategorySerializer

class CategoryDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Category.objects.all()
serializer_class = CategorySerializer

class BalanceListView(generics.ListCreateAPIView):
queryset = Balance.objects.all()
serializer_class = BalanceSerializer

3. YAGNI (You Ain’t Gonna Need It)

Don’t add functionality until it’s necessary.

Focus on current requirements rather than future possibilities.

Example: Simple User Profile

Begin with a straightforward user model and add complexity only when necessary.

# models.py

class User(AbstractUser):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
email = models.EmailField(unique=True)

def __str__(self):
return self.username

4. Separation of Concerns

Different parts of your code should have different responsibilities.

Django’s MVC (Model-View-Controller) architecture separates data handling (Models), user interface (Templates), and application logic (Views).

Example: Separating Business Logic from Views

Keep business logic in models or services instead of views.


# models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

class AnstractModel(models.Model):
class Meta:
abstract = True

deleted = models.BooleanField(default=False)
date_created = models.DateTimeField('Date created', auto_now_add=True)
date_last_updated = models.DateTimeField('Data last updated', auto_now=True)

def __id__(self) -> int:
return self.id

def delete(self, *args, **kwargs):
self.deleted = True
self.save()

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

....

# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import generics
from django.shortcuts import get_object_or_404
from .models import Expense, Category, Balance, User
from .serializers import ExpenseSerializer, CategorySerializer, BalanceSerializer

class ExpenseListView(generics.ListCreateAPIView):
queryset = Expense.objects.all()
serializer_class = ExpenseSerializer

class ExpenseDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Expense.objects.all()
serializer_class = ExpenseSerializer

class CategoryListView(generics.ListCreateAPIView):
queryset = Category.objects.all()
serializer_class = CategorySerializer

class CategoryDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Category.objects.all()
serializer_class = CategorySerializer

class BalanceListView(generics.ListCreateAPIView):
queryset = Balance.objects.all()
serializer_class = BalanceSerializer

class UserTotalExpensesView(APIView):
def get(self, request, user_id):
user = get_object_or_404(User, id=user_id)
total_expenses = user.get_total_expenses()
return Response({'total_expenses': total_expenses})

# urls.py
from django.urls import path
from .views import ExpenseListView, ExpenseDetailView, CategoryListView, CategoryDetailView, BalanceListView, UserTotalExpensesView

urlpatterns = [
path('expenses/', ExpenseListView.as_view(), name='expense-list'),
path('expenses/<int:pk>/', ExpenseDetailView.as_view(), name='expense-detail'),
path('categories/', CategoryListView.as_view(), name='category-list'),
path('categories/<int:pk>/', CategoryDetailView.as_view(), name='category-detail'),
path('balances/', BalanceListView.as_view(), name='balance-list'),
path('users/<int:user_id>/total-expenses/', UserTotalExpensesView.as_view(), name='user-total-expenses'),
]

5. SOLID Principles

SOLID is an acronym for five design principles intended to make software designs more understandable, flexible, and maintainable.

S: Single Responsibility Principle (SRP)

Each class should have one responsibility.

# model/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

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


class AbstractModel(models.Model):
class Meta:
abstract = True

deleted = models.BooleanField(default=False)
date_created = models.DateTimeField('Date created', auto_now_add=True)
date_last_updated = models.DateTimeField('Data last updated', auto_now=True)

def __id__(self) -> int:
return self.id

def delete(self, *args, **kwargs):
self.deleted = True
self.save()

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


class User(AbstractUser):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
email = models.EmailField(unique=True)

def __str__(self):
return self.username


class Category(AbstractModel):
class Meta:
verbose_name = "Category"
verbose_name_plural = "Categories"
db_table = "db_category"

name = models.CharField('Name', max_length=100, unique=True)
description = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE)

def __str__(self):
return self.name


class Expense(AbstractModel):
class Meta:
verbose_name = "Expense"
verbose_name_plural = "Expenses"
db_table = "db_expense"

user = models.ForeignKey(User, on_delete=models.CASCADE)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
amount = models.DecimalField(max_digits=10, decimal_places=2)
description = models.TextField()
date = models.DateField()


class Balance(models.Model):
class Meta:
verbose_name = "Balance"
verbose_name_plural = "Balances"
db_table = "db_balance"

total_balance = models.DecimalField(max_digits=10, decimal_places=2)

O: Open/Closed Principle (OCP)

Software entities should be open for extension but closed for modification.

# middleware.py
from django.http import HttpResponse

class BaseMiddleware:
def process_request(self, request):
raise NotImplementedError

class AuthMiddleware(BaseMiddleware):
def process_request(self, request):
if not request.user.is_authenticated:
return HttpResponse('Unauthorized', status=401)

L: Liskov Substitution Principle (LSP)

Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.

# models.py

class Notification:
def send(self):
raise NotImplementedError

class EmailNotification(Notification):
def send(self):
print("Sending email")

class SMSNotification(Notification):
def send(self):
print("Sending SMS")

def notify(notification: Notification):
notification.send()

# Usage
email_notification = EmailNotification()
sms_notification = SMSNotification()

notify(email_notification) # Output: Sending email
notify(sms_notification) # Output: Sending SMS

I: Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they do not use.

# Interface Segregation by splitting larger views
from django.views import View
from django.http import JsonResponse

class CreateMixin:
def create(self, request, *args, **kwargs):
return JsonResponse({'message': 'Create not implemented'}, status=405)

class ReadMixin:
def read(self, request, *args, **kwargs):
return JsonResponse({'message': 'Read not implemented'}, status=405)

class UpdateMixin:
def update(self, request, *args, **kwargs):
return JsonResponse({'message': 'Update not implemented'}, status=405)

class DeleteMixin:
def delete(self, request, *args, **kwargs):
return JsonResponse({'message': 'Delete not implemented'}, status=405)

class MyView(CreateMixin, ReadMixin, View):
def read(self, request, *args, **kwargs):
return JsonResponse({'message': 'Reading data'})

D: Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions.

class PaymentService:
def process_payment(self):
raise NotImplementedError

class StripePaymentService(PaymentService):
def process_payment(self):
print("Processing payment with Stripe")

class PayPalPaymentService(PaymentService):
def process_payment(self):
print("Processing payment with PayPal")

# views.py
from .services import PaymentService

class PaymentView(View):
def __init__(self, payment_service: PaymentService):
self.payment_service = payment_service

def post(self, request, *args, **kwargs):
self.payment_service.process_payment()
return JsonResponse({'message': 'Payment processed'})

# Usage
stripe_service = StripePaymentService()
paypal_service = PayPalPaymentService()

stripe_payment_view = PaymentView(stripe_service)
paypal_payment_view = PaymentView(paypal_service)

--

--

denisa_dev
Django Unleashed

🎯 Experienced and Passionate Software Engineer | Results-Oriented