In this chapter, we are going to create the CRUD for the blog.

Maintaining a clean, readable, and modular Django project is no small task. If you've ever been lost among duplicated views, endless functions, or classes that do too much, you know what I'm talking about.

In my projects—especially when developing an online store with multiple product types and payments—I discovered that true "Clean Code" in Django begins with one word: modularization.

What it means to write clean code in Django

Core principles: clarity, modularity, and maintenance

Clean code isn't about writing beautifully; it's about writing code that others (including you in six months) can understand effortlessly.
In Django, that translates to organizing the project into coherent modules, keeping views small, and reusing common logic.

Why "Clean Code" isn't just aesthetics

A poorly structured Django project can still work, but it will be difficult to test, scale, and maintain. Writing clean code involves making design decisions that reduce coupling, avoid repetition, and follow principles like DRY (Don’t Repeat Yourself) or KISS (Keep It Simple, Stupid).

Modularization: the foundation of clean code in Django

From function-based views to class-based views (CBV)

This article talks about the importance of modularization and the use of classes as key components; as you know, in Django, we have two ways to define views, one of which is through methods:

def post_list(request):
    posts = Post.objects.all().order_by('-id')
    return render(request, 'post_list.html', {'posts': posts})

It works, yes, but it scales poorly. If you need shared behavior (for example, filtering, sorting, or applying permissions), you end up copying code.

The alternative—and my personal recommendation—is to use Class-Based Views (CBV), which allow you to inherit behavior and centralize common logic:

class PostListView(ListView):
    model = Post
    template_name = 'post_list.html'      # por defecto: app/post_list.html
    context_object_name = 'posts'         # por defecto: object_list
    ordering = ['-id']

The simple act of inheriting from `ListView` reduces lines of code, improves readability, and gives you a predictable structure.

As a recommendation, you should always use Class-Based Views whenever possible; I personally believe that classes are among the best features we have in any programming language, as they are a key component in the modularization of our applications.

Practical example: refactoring a payment view in Django

In my case, I had two very similar views: one for paying for books and one for products.
Both received the same parameters (`order_id`, `book_id` or `product_id`, `type`) and performed practically the same logic.

Suppose the following class:

class PaymentBookView(LoginRequiredMixin, View, BasePayment):
    def __init__(self):
        # super().__init__()
        BasePayment.__init__(self)
        
    def _redirect_or_json(self, request, url_name, kwargs):
        url = reverse(url_name, kwargs=kwargs)
        if request.headers.get("Content-Type") == "application/json":
            return JsonResponse({"redirect": url})
        return redirect(url, kwargs)
    
    def get(self, request, order_id:str, book_id:int, type:str):
        return self._process(request, order_id, book_id, type)
    
    def post(self, request, order_id:str, book_id:int, type:str):
        return self._process(request, order_id, book_id, type) 
    
    def _process(self, request, order_id:str, book_id:int, type:str):
        #TODO revisar que NO compre el mismo producto 2 veces
     
        # si la ordenID en la URL no es valida, lo busca en el request, caso Stripe   
        # http://127.0.0.1:8000/store/payment/orderID/2/stripe?order_id=cs_test_a*pR
        if order_id == 'orderID':
            *
     
        # procesamos la orden
        response = self.process_order(order_id, type)

        # Error en la orden
        if response == False:
            return self._redirect_or_json(request, "s.payment.error", message_error=self.message_error)
        
        #usuario auth
        user = request.user 
        
        # buscamos el producto
        try:
            book = Book.objects.get(id=book_id)
        *

        # creamos el producto si todo esta ok
        payment = Payment.objects.create(
            user=user,
            type=self.type,  
            coupon=None,  
            orderId=order_id,
            price=self.price,
            trace=self.trace,  
            content_type=ContentType.objects.get_for_model(book),
            object_id=book.id
        )

        return self._redirect_or_json(request, "s.payment.success", payment_id=payment.id)

Which is part of the complete course and book to create an online store in Django; some implementations have been omitted, but the essential parts can be seen in the code above; the problem is that there is a lot of repeated code in the previous class, whose routes are:

# pagar un producto/book
path('payment/<str:order_id>/<int:book_id>/<str:type>', PaymentBookView.as_view(), name='s.payment'),
# pagar un producto
path('product/payment/<str:order_id>/<int:product_id>/<str:type>', PaymentProductView.as_view(), name='s.product.payment'),

Ultimately, the parameters indicated are the identifiers for purchasable items such as a book and something called a product; but the logic is the same; how can we optimize this to truly be a modular class? Very easily, we can create another class that holds this common behavior, but how do we handle the parameters:

content_type=ContentType.objects.get_for_model(product),
   object_id=product.id
)

Solution: a base class that unifies everything

The key was to create a base class that centralized the common behavior and left only the specific details to each model:

class BasePaymentView(LoginRequiredMixin, View, BasePayment):
    model = None          # el modelo (Book o Product)
    lookup_field = "id"   # campo de búsqueda (generalmente id)
    url_kwarg = None      # el parámetro en la URL (book_id, product_id)

    def __init__(self):
        BasePayment.__init__(self)

    def _redirect_or_json(self, request, url_name, kwargs):
        url = reverse(url_name, kwargs=kwargs)
        if request.headers.get("Content-Type") == "application/json":
            return JsonResponse({"redirect": url})
        return redirect(url, kwargs)

    def get(self, request, order_id: str, type: str, kwargs): # parametro extra (kwargs) que es el de <int:product_id>/<int:book_id>
        return self._process(request, order_id, type, kwargs) 

    def post(self, request, order_id: str, type: str, kwargs): # parametro extra (kwargs) que es el de <int:product_id>/<int:book_id>
        return self._process(request, order_id, type, kwargs)

    def _process(self, request, order_id: str, type: str, kwargs):
        # si la ordenID en la URL no es valida, lo busca en el request (caso Stripe)   
        if order_id == 'orderID':
            *
     
        # procesamos la orden
        response = self.process_order(order_id, type)
        if response is False:
            return self._redirect_or_json(request, "s.payment.error", message_error=self.message_error)

        # usuario auth
        user = request.user 

        # buscamos el objeto parametro extra <int:product_id>/<int:book_id>
        pk = kwargs.get(self.url_kwarg)
        obj = get_object_or_404(self.model, {self.lookup_field: pk})

        # creamos el payment
        payment = Payment.objects.create(
            user=user,
            type=self.type,
            coupon=None,
            orderId=order_id,
            price=self.price,
            trace=self.trace,
            content_type=ContentType.objects.get_for_model(obj),
            object_id=obj.id
        )

        return self._redirect_or_json(request, "s.payment.success", payment_id=payment.id)

As you can see, this new class inherits all the common behavior from the previous two, but the dynamic part is handled through attributes to specify exactly what is being purchased:

# Procesa la compra de un Libro
class PaymentBookView(BasePaymentView):
    model = Book
    url_kwarg = "book_id"

# Procesa la compra de un Producto
class PaymentProductView(BasePaymentView):
    model = Product
    url_kwarg = "product_id"

In this simple way, we managed to modularize this component of our application; in the aforementioned training, we strive to make everything as modular as possible. Online stores are one of the developments that can easily get out of control, as they have to handle many things such as payment gateways, configuration parameters, error screens, logs, and coupons, and that's why in this training, we present an efficient and modular approach that you can use not only in Django projects with Python web but in all types of projects.

How to adapt it for other models

If tomorrow I wanted to add a new payment type (e.g., memberships), I would only need a new class that specifies its model.
This drastically reduces duplicated code and makes the business logic easier to maintain and test.

Clean Code Principles applied to Django

DRY, KISS, and YAGNI in practice

  • DRY: Avoid repeating code. If you copy and paste a piece of logic twice, turn it into a reusable method or class.
  • KISS: Keep views simple and with a clear purpose.
  • YAGNI (You Ain't Gonna Need It): Don't implement things "just in case." Django already has a lot solved for you.

Naming, PEP8, and consistency in your views and models

  • Follow PEP8 style guidelines and use descriptive names. A class named `PaymentBookView` communicates much more than `BookPay`.
  • Visual consistency (order of imports, spacing, comments) is not trivial: it reduces the cognitive effort when reading the code.

How to keep your Django project scalable and clean

Testing, continuous refactoring, and code reviews

Clean code isn't written just once: it's maintained.
In my projects, I usually do small refactorings after each new module. A failing test is often an opportunity to clean up.

Use of mixins, utils, and custom libraries

Group cross-cutting logic (authentication, permissions, validations) into mixins or `utils.py` modules.
This prevents your views from growing uncontrollably and improves error traceability.

Conclusion

Modularization not only improves readability: it reduces the stress of maintenance.
Each class or module should have a clear responsibility and a name that expresses it.
In my experience, Django applications that follow this approach scale better and require less debugging time.

Clean code is not a final goal, it is a constant process of improvement.
And Django—due to its structure based on classes, signals, and independent apps—is the perfect environment to apply it.

In summary:

  • Modularization and reuse are key in large projects.
  • AI can be an excellent support, as long as we are clear about what we want to achieve.
  • The most solid solution is the one that sacrifices a little flexibility to gain in security and clarity.
  • So, that was the context I wanted to give you for YouTube. I hope you enjoy this free class, which is part of the complete course. See you in the next section.

Frequently Asked Questions (FAQ)

  • Which Clean Code principles apply best to Django?
    Mainly DRY, KISS, and Single Responsibility. Each view or model should have one clear purpose.
  • When is it convenient to use class-based views?
    Whenever you need reusable or extensible behavior: pagination, permissions, or dynamic filters.
  • How to detect that your Django project needs refactoring?
    If it's hard to modify something without breaking another part, or if you see the same block of code in more than two files, it's time to clean up.

I agree to receive announcements of interest about this Blog.

Refactoring like a pro in Django: Clean up your code by modularizing.

| 👤 Andrés Cruz

🇪🇸 En español