avatarBuilescu Daniel

Summary

The provided content offers advanced Django development insights, showcasing 10 essential tips and tricks to enhance Django expertise, including ORM optimizations, profiling with Django Debug Toolbar, power user shell enhancements, middleware techniques, Django Channels for asynchronous tasks, templating engine secrets, security best practices, signal usage, authentication system customization, and performance optimization tools.

Abstract

The article "The Django Developer’s Toolkit: 10 Must-Know Tips and Tricks" serves as a comprehensive guide for Django developers seeking to elevate their skills and optimize their applications. It delves into the intricacies of Django's Object-Relational Mapping (ORM) system, revealing advanced features such as conditional expressions, F() expressions, and Prefetch objects for query optimization. The piece also highlights the Django Debug Toolbar as a robust profiling tool for identifying performance bottlenecks and inefficient database queries. For developers, the article suggests supercharging the Django shell with IPython and Django Extensions' shell_plus for a more powerful and efficient development environment. It further explores the strategic use of middleware for reusable components and context-specific logic, as well as the application of Django Channels for managing asynchronous tasks and WebSockets. The advanced capabilities of Django's templating engine, including template inheritance, custom template tags, and context processors, are also discussed. On the security front, the article uncovers Django's lesser-known protections against clickjacking, host header validation, and secure proxy configurations. It emphasizes the importance of custom user models and backend authentication systems to tailor the framework to specific project needs. Finally, the article introduces tools like Django DevServer, Silk, and Django-Test-Without-Migrations to help developers optimize their Django applications' performance.

Opinions

  • The author, Builescu Daniel, positions themselves as an expert in the field, offering valuable insights based on their experience as an ex-Google developer proficient in Python and Django.
  • The article promotes the idea that mastering advanced Django features can lead to more efficient, maintainable, and secure web applications.
  • There is an emphasis on the importance of community and continuous learning, with invitations to follow the author on Medium, join a Discord community for further discussion, and support the author's work through Patreon.
  • The author suggests that leveraging Django's full potential requires an understanding of its less commonly used features and third-party tools, which can significantly enhance development workflows and application performance.
  • The article encourages a proactive approach to security, advocating for the use of Django's built-in security features to protect against common web vulnerabilities.
  • By providing examples and code snippets, the author demonstrates a commitment to practical, hands-on learning and encourages readers to apply the tips and tricks directly to their Django projects.

The Django Developer’s Toolkit: 10 Must-Know Tips and Tricks

Unlock your full Django potential: Master advanced techniques and discover hidden features with these 10 must-know tips and tricks.

Welcome to “The Django Developer’s Toolkit: 10 Must-Know Tips and Tricks”! In this advanced guide, we delve into little-known but immensely powerful features and techniques that can elevate your Django expertise to new heights. Whether you’re a seasoned Django professional or an intermediate developer looking to level up, this article promises to offer something of value.

Remember, Django is a versatile beast, and taming it requires an intimate understanding of its lesser-known corners. If you’re excited to unravel the Django mystery with me, do follow me here on Medium and enable notifications, so you don’t miss out on any future articles.

And the learning doesn’t stop here! I invite you to join our ever-growing community on Discord where we dissect intriguing concepts, answer pressing questions, and foster a learning environment that encourages knowledge sharing.

I can’t wait to embark on this Django journey with you. Let’s dive in!

1. Unconventional ORM Tricks

In Django, the Object-Relational Mapping (ORM) system is a vital and powerful tool that allows us to interact with our database, like querying, in a Pythonic way. But beyond its common usage, Django ORM houses some advanced features that, when utilized correctly, can significantly optimize our queries and open doors to more complex data manipulations. Let’s delve into these hidden gems.

  • Conditional Expressions: Django ORM’s conditional expressions let you use if…elif…else logic in your queries. The Case and When classes can create powerful SQL case statements. Let’s see an example:
from django.db.models import Case, When, Value, CharField
from .models import Product

products = Product.objects.annotate(
    product_type=Case(
        When(product_code__startswith='1', then=Value('ELECTRONIC')),
        When(product_code__startswith='2', then=Value('FURNITURE')),
        default=Value('OTHER'),
        output_field=CharField(),
    )
)

In the example above, we classify the products into different types based on the starting digit of their product code. This operation is performed at the database level using the SQL CASE statement, thanks to Django ORM’s conditional expressions.

  • F() Expressions: F() expressions allow you to refer to model field values and perform database operations using them. They can be particularly useful for making atomic updates. Here’s an example:
from django.db.models import F
from .models import Product

Product.objects.update(stock=F('stock') - 1)

In this example, the F() expression is used to decrement the stock of all products by one directly in the database, which is more efficient and safe against race conditions.

  • Prefetch Objects: The prefetch_related() function is a godsend when it comes to optimizing and reducing the number of database hits. But the real magic happens when you use it with Prefetch objects to further customize the prefetching of related objects:
from django.db.models import Prefetch
from .models import Author, Book

authors = Author.objects.prefetch_related(
    Prefetch('books', queryset=Book.objects.filter(title__contains='Django'))
)

In the above example, for each author in authors, the related books are prefetched from the database, but only if their title contains 'Django'. This can significantly optimize database queries when dealing with related objects.

By leveraging these unconventional ORM tricks, you can optimize your Django applications, make them more efficient and maintainable, while also showcasing your expertise in Django ORM. On to the next trick!

2. Profiling with Django Debug Toolbar

When it comes to debugging and performance tuning, Django offers a powerful tool named Django Debug Toolbar. This robust toolset extends beyond its default role of being a mere debug assistant; it serves as an efficient profiler helping to optimize and speed up your Django applications. In this section, let’s take a look at how to make the most of this toolkit.

Installing Django Debug Toolbar is straightforward. You can add it to your project by running pip install django-debug-toolbar, then adding it to your INSTALLED_APPS and MIDDLEWARE settings. But let's move past the basics, and talk about how it can be a game-changer for your Django project.

  1. SQL Optimization: One of the key features of the Debug Toolbar is its ability to show all the SQL queries made during a request. This feature is not only useful for debugging but also for identifying areas where you can optimize your queries. The toolbar breaks down the queries, showing you exactly when and where each query was executed, and how long it took. This provides an opportunity to spot redundant queries, N+1 problems, or inefficient database hits that could be improved using Django ORM’s advanced features like select_related or prefetch_related.
  2. Cache Efficiency: Django Debug Toolbar also provides insights into your cache utilization. If your project makes use of Django’s caching framework, the Debug Toolbar can show you which parts of a page are being cached, how effective the cache is, and where potential performance gains might be found.
  3. Profiling View Functions: Beyond its standard panels, Django Debug Toolbar can be extended with third-party panels. One such panel is the Profiling panel, which can be a great tool to identify bottlenecks in your view functions. Once installed, it can provide a breakdown of time spent in each function during a request, helping you spot the areas that need optimization.
pip install django-devserver

After installing, add devserver to your INSTALLED_APPS, and devserver.middleware.DevServerMiddleware to your MIDDLEWARE. The profiling panel should now be available in your Debug Toolbar.

By mastering the Django Debug Toolbar and its extensions, you’re not just squashing bugs, you’re actively improving the speed and efficiency of your application. This translates into a better user experience, more manageable code, and a new set of skills in your Django toolkit. Now, let’s move on to the next tip.

3. Power User Django Shell

The Django shell is a powerful tool for performing ad-hoc operations, testing, and debugging. But have you ever felt it could be… more? With some additional tools and configurations, you can transform the Django shell into an incredibly powerful ally for your day-to-day development. In this section, we’ll dive into some of the ways to get more out of Django’s shell, namely with the use of IPython and Django Extensions’ shell_plus.

1.Supercharging Django Shell with IPython: If you’re not already using IPython as your default Django shell, you’re in for a treat. IPython offers a slew of improvements over the standard Python shell, including syntax highlighting, better traceback outputs, and magic commands. To get IPython, simply install it with pip (pip install ipython). Django will automatically use it for the shell if it's available.

2.Django Extensions and shell_plus: The Django Extensions is a collection of third-party plug-ins that extend Django’s built-in functionality, and among its tools is shell_plus. Shell_plus is an enhanced version of Django shell. It auto-imports all your models, reducing the time spent on typing repetitive import statements. To utilize shell_plus, first install Django Extensions with pip (pip install django-extensions), add 'django_extensions' to your INSTALLED_APPS, and then start the enhanced shell with python manage.py shell_plus.

# settings.py
INSTALLED_APPS = [
    ...,
    'django_extensions',
    ...
]

# Terminal
python manage.py shell_plus

3.Customizing shell_plus: The true power of shell_plus comes from its customizability. For example, you can create a .pythonrc.py file in your home directory, which shell_plus will automatically load upon startup. In this file, you can put any Python code: import commonly used utilities, set up environment-specific settings, or even change how Django objects are displayed in the shell. The possibilities are virtually endless.

Here is an example of how you can customize the display of your Django objects in shell_plus:

# .pythonrc.py
from django.conf import settings
if 'django_extensions' in settings.INSTALLED_APPS:
    from django.db.models.query import QuerySet
    def qs_repr(self):
        return repr(list(self))
    QuerySet.__repr__ = qs_repr

This will make shell_plus display QuerySets as lists of objects, rather than just the query string, which can make testing and debugging in the shell easier.

Mastering your tools is an important part of becoming a more efficient developer. By supercharging your Django shell with IPython and shell_plus, and tailoring it to your needs, you can make your Django development experience smoother, more pleasant, and more productive. Let’s carry on to the next section where we will explore more Django power tips.

4. Advanced Django Middleware Techniques

Middleware in Django is a powerful tool, acting as a series of hooks into Django’s request/response process. They allow us to apply changes or effects to requests and responses globally, across multiple views. You’re probably already familiar with some built-in middleware provided by Django like AuthenticationMiddleware or SessionMiddleware. But did you know you can create your own middleware to create reusable components or context-specific middleware? Let's deep dive into these advanced middleware techniques.

Creating Reusable Middleware Components: One of the beauties of Django middleware is its reusability. Let’s say you have multiple views that need to perform a certain action on a request, such as checking if a user has accepted the terms of service. Rather than repeating the same code in multiple views, you can write a middleware to do this once and apply it across the board.

Here’s an example of a simple middleware component:

class TermsOfServiceMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # Before the view (and other middleware) are called.
        if not request.user.is_anonymous and not request.user.has_accepted_tos:
            return redirect('terms-of-service')

        response = self.get_response(request)

        # After the view is called.
        return response

With this middleware added to your MIDDLEWARE settings, Django will ensure that every authenticated user has accepted your terms of service, before accessing any view.

Context-Specific Middleware: Middleware does not always have to apply to every single request. Sometimes, you might want to apply middleware only to certain views, or based on some condition in the request.

Django 1.10 introduced the MiddlewareMixin class, which allows middleware to be used as view decorators. This means that you can create middleware that only applies to certain views.

Here’s how you might use this:

from django.utils.deprecation import MiddlewareMixin

class SpecialCaseMiddleware(MiddlewareMixin):
    def process_view(self, request, view_func, view_args, view_kwargs):
        # If this is a view we're interested in...
        if view_func.__name__ == 'special_case_view':
            # Do some processing...
            request.special_case = 'This is a special case!'

@SpecialCaseMiddleware
def special_case_view(request):
    ...

This introduces another level of flexibility to your Django projects, enabling you to precisely control the flow of requests and responses. It also keeps your views more focused on their main tasks, which is a win for code maintainability. The next section will give us more insights into other must-know tips and tricks of Django. Let’s keep the learning journey moving.

5. Exploiting Django Channels for Asynchronous Processes

Django Channels is a fantastic tool that extends Django to handle WebSockets, long-poll HTTP, and other asynchronous protocols. While it’s typically associated with real-time communication features, like chat apps, Channels can also be leveraged to manage asynchronous tasks in your Django application. Let’s take a look at how you can exploit this powerful feature.

Running Asynchronous Tasks: Django Channels uses an event-driven architecture powered by ASGI (Asynchronous Server Gateway Interface). This makes it a great tool for managing tasks that could benefit from asynchronous execution. For example, if you have a resource-intensive task that you don’t want to block your server’s response, you can offload that task to Django Channels.

from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync

channel_layer = get_channel_layer()

def expensive_view(request):
    # Offload the expensive task to the channel layer
    async_to_sync(channel_layer.send)('expensive_task', {'type': 'expensive.task'})
    return HttpResponse("Task is being processed!")

In the consumer:

class ExpensiveConsumer(AsyncConsumer):
    async def expensive_task(self, event):
        # Your expensive task here
        pass

This will allow your view to quickly respond to the user while the expensive task is processed in the background.

Exploiting WebSockets: Beyond asynchronous tasks, Django Channels opens up the power of WebSockets. WebSockets create a persistent connection between the client and server, allowing real-time bidirectional communication. This is great for features that need to push updates to the client, like a notification system.

Here’s a simple example of how you might set up a consumer to handle WebSocket connections:

from channels.generic.websocket import AsyncWebsocketConsumer
import json

class NotificationConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        await self.accept()

    async def disconnect(self, close_code):
        pass

    async def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']

        # Send message to WebSocket
        await self.send(text_data=json.dumps({
            'message': message
        }))

In this section, we’ve just touched the surface of what you can achieve with Django Channels. When used creatively, it opens up a world of possibilities for improving your application’s performance and user experience. Let’s keep going and discover more Django tips and tricks in the following sections.

6. Django Templating Engine Secrets

Django’s templating engine is one of the core features that makes it such a powerful and flexible framework. Beyond its basic usage, the Django template engine offers some advanced features that are less commonly known but incredibly handy. In this section, let’s explore some of these features, such as template inheritance, custom template tags, and template context processors.

Template Inheritance: Template inheritance is a powerful tool that allows you to create a base skeleton template containing all the common elements of your site and defines blocks that child templates can override. It helps in reusing code and keeping your templates organized.

Let’s consider a base template (base.html):

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}{% endblock %}</title>
</head>
<body>
    {% block content %}{% endblock %}
</body>
</html>

A child template might look like this (child.html):

{% extends "base.html" %}

{% block title %}My Amazing Site{% endblock %}

{% block content %}
<!-- Your content here -->
{% endblock %}

By using the {% extends %} tag, we tell Django that child.html is a child of base.html and will inherit its structure.

Custom Template Tags: Django’s built-in template tags like {% if %}, {% for %} etc., cover a wide range of use cases, but sometimes you need to perform operations that are unique to your project. This is where custom template tags come in.

To create a custom template tag, you first need to create a Python module in a Django app and define your custom function there.

# myapp/templatetags/my_custom_tags.py

from django import template

register = template.Library()

@register.filter
def lower(value):  # Converts a string into all lowercase
    return value.lower()

In your template, you would load the custom tags and then use them:

{% load my_custom_tags %}

<p>{{ my_string|lower }}</p>

Template Context Processors: A context processor is a Python function that takes the request object as an argument and returns a dictionary to add to the context. They’re specified using the TEMPLATE_CONTEXT_PROCESSORS setting and are a fantastic way to make sure certain data is available to all your templates.

Let’s create a context processor that adds the current year to every template:

# myapp/context_processors.py

from datetime import datetime

def current_year(request):
    return {'current_year': datetime.now().year}

Make sure you’ve added the context processor to your settings.py:

TEMPLATES = [
    {
        # ...
        'OPTIONS': {
            'context_processors': [
                # ...
                'myapp.context_processors.current_year',
            ],
        },
    },
]

Now, you can access the current year in your templates:

<footer>
    &copy; {{ current_year }} My Amazing Site
</footer>

These features of the Django templating engine allow you to craft more dynamic, adaptable, and maintainable templates. By mastering these techniques, you’re taking another step towards becoming a Django power user.

7. Maximize Security with Django’s Lesser-known Protections

In the ever-evolving world of web development, security is paramount. Django comes with a rich set of security features that, when used correctly, can help you build robust, secure web applications. In this section, we’ll uncover some of Django’s lesser-known, yet highly impactful, security features, including clickjacking protection, host header validation, and the SECURE_PROXY_SSL_HEADER setting.

Clickjacking Protection: Clickjacking is a malicious technique where an attacker tricks a user into clicking something different from what the user perceives, potentially leading to unwanted actions. Django provides a middleware to protect against clickjacking by setting the X-Frame-Options header in HTTP responses. To activate this protection, add 'django.middleware.clickjacking.XFrameOptionsMiddleware' to your MIDDLEWARE settings:

MIDDLEWARE = [
    # ...
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    # ...
]

By default, the header is set to DENY, meaning the page cannot be displayed in a frame. You can adjust this using the X_FRAME_OPTIONS setting.

Host Header Validation: Django uses the host header in the incoming HTTP request to construct URLs in certain cases. However, a maliciously crafted host header could lead to vulnerabilities. From version 1.10.3 onwards, Django validates the host header against the ALLOWED_HOSTS setting and returns a 400 Bad Request response if the host header doesn’t match any pattern in the list.

ALLOWED_HOSTS = ['mywebsite.com', 'www.mywebsite.com']

It’s important to keep this setting up-to-date as your project grows or changes environments.

SECURE_PROXY_SSL_HEADER Setting: If your Django app is behind a proxy that’s serving HTTPS, Django needs to know when a request is secure so it can generate appropriate URLs and avoid redirect loops. In this scenario, you can use the SECURE_PROXY_SSL_HEADER setting. This is a tuple representing a header/value pair that a proxy sets when handling HTTPS requests:

SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

Be cautious with this setting; only use it when you control both the Django app and the proxy, as it can lead to security issues if misconfigured.

These security features form an integral part of Django’s built-in arsenal against common web threats. While they might not be as commonly discussed as CSRF or XSS protections, their correct application can provide a significant boost to your application’s security. Always remember, a secure application is a trustworthy application!

8. Mastering Django Signals

Django signals are a type of hook for customizing Django’s behavior at various points of its execution. Signals allow certain senders to notify a set of receivers when specific actions have taken place. They’re used for decoupled, lightweight, and asynchronous communication between different parts of your application. In this section, we will look into advanced techniques for registering signals with decorators, preventing duplicate signals, and using custom signals for cleaner and more decoupled code.

Advanced Signal Registration with Decorators: While you can connect signals and receivers using the connect() method, using decorators can simplify the process and make your code cleaner. Here is an example using the @receiver decorator:

from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import MyModel

@receiver(post_save, sender=MyModel)
def mymodel_saved(sender, **kwargs):
    print("A MyModel instance was saved!")

In this example, the mymodel_saved function will be called each time a MyModel instance is saved.

Preventing Duplicate Signals: If your application runs in multiple processes (for example, due to multiple worker processes in a WSGI server), you might find that your signals are being executed more than once for a single event. This is because each process has its own memory space and, thus, its own signal handlers. To prevent this, ensure that you are connecting your signals in a module-level loaded module (such as in models.py or in a AppConfig ready method).

Using Custom Signals for Cleaner, More Decoupled Code: Django includes a set of built-in signals allowing certain senders to notify a set of receivers when specific actions have taken place. But you can also create your own signals. Custom signals help decouple applications and can make your code cleaner:

from django.dispatch import Signal

# Defining a custom signal
my_signal = Signal(providing_args=["arg1", "arg2"])

# Elsewhere in your code...
my_signal.send(sender=this_module, arg1='Hello', arg2='World')

Here, my_signal is a custom signal. It can be sent from anywhere in your code using the send() method, and any receiver connected to this signal will receive it.

By fully understanding and using Django signals, you can write more decoupled, maintainable, and clean code. Now, we have covered a lot of ground, but our journey through Django’s advanced features isn’t over yet.

9. Secrets of Django’s Authentication System

Django’s built-in authentication system is robust, secure, and extremely flexible. You probably already use it for managing user accounts, logins, and permissions. But let’s delve a little deeper and discover some powerful, less-frequently used features, such as custom user models, backend authentication, and other hidden gems.

Custom User Models: While Django’s default User model is comprehensive, it may not fit your needs exactly. Django allows you to substitute the default User model with a custom model. This is useful when you want to add extra fields, like a bio or a phone number, or if you want the ‘username’ field to be an email.

Here is a simple example of a custom user model:

from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):
    phone_number = models.CharField(max_length=20)
    bio = models.TextField(blank=True)

To tell Django to use this model as the user model, you would add the following to your settings file:

AUTH_USER_MODEL = 'myapp.CustomUser'

Backend Authentication: Django’s authentication system is backed by a pluggable backend architecture. This means you can add multiple authentication methods, or even write your own. For example, you might have users in a legacy system you need to authenticate against, or maybe you want to authenticate users based on an OAuth token.

Here is an example of a simple authentication backend:

from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model

class EmailBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        UserModel = get_user_model()
        try:
            user = UserModel.objects.get(email=username)
        except UserModel.DoesNotExist:
            return None

        if user.check_password(password):
            return user

    def get_user(self, user_id):
        UserModel = get_user_model()
        try:
            return UserModel.objects.get(pk=user_id)
        except UserModel.DoesNotExist:
            return None

This backend authenticates the user based on their email and password. To use it, you would add the following to your settings file:

AUTHENTICATION_BACKENDS = ['path.to.EmailBackend']

By extending and customizing Django’s authentication system, you can integrate Django more tightly with your specific use case, while still benefiting from Django’s robust, secure authentication framework.

10. Optimize Django with Lesser-Known Tools

Performance optimization is an often overlooked aspect of Django development, but it can yield significant improvements to your application’s speed and efficiency. Thankfully, the Django ecosystem is teeming with powerful tools that can help you fine-tune your app’s performance. Let’s explore three less-common tools you can use to optimize Django: Django DevServer, Silk, and Django-Test-Without-Migrations.

Django DevServer: This is a drop-in replacement for Django’s built-in development server. It provides additional features like SQL query logging, cache utilization, and CPU and memory load monitoring, helping you identify performance bottlenecks.

To use Django DevServer, you’d first install it with pip:

pip install git+https://github.com/dcramer/django-devserver.git

Then you add it to your INSTALLED_APPS list in settings.py:

INSTALLED_APPS = [
    ...,
    'devserver',
    ...
]

Silk: Silk is a live profiling and inspection tool for Django. It intercepts and stores HTTP requests and database queries before presenting them in a user-friendly interface. You can visualize where your application spends time, allowing you to optimize database queries, view stack traces, and investigate performance problems.

To install Silk, use pip:

pip install django-silk

Then, you’d add it to your middleware and installed apps:

MIDDLEWARE = [
    ...,
    'silk.middleware.SilkyMiddleware',
    ...
]

INSTALLED_APPS = [
    ...,
    'silk',
    ...
]

Django-Test-Without-Migrations: While not a performance optimization for your live application, this tool can significantly speed up your test suite by skipping migrations. It’s particularly useful if you have a large number of migrations that are slowing down your tests.

To install Django-Test-Without-Migrations, use pip:

pip install django-test-without-migrations

Then you can run your tests without migrations using the test_without_migrations management command:

python manage.py test_without_migrations

These tools can shine a light on the darker corners of your Django application’s performance, guiding you towards a faster, more efficient application.

Well, that wraps up our deep dive into some of Django’s more advanced features and hidden gems. I hope that you have discovered new tools, techniques, and knowledge that you can apply to elevate your Django development to the next level. Remember, learning is a continuous process, and there are always more secrets to uncover in the world of Django.

If you found this article helpful, don’t forget to follow me here on Medium to receive notifications for my future articles. There’s always more to explore and learn, and I’d love to continue this journey together.

For a more interactive and community-based learning experience, consider joining my Discord server. It’s an engaging place where you can ask questions, share your projects, and learn from other Django enthusiasts.

Lastly, if you find my content valuable and wish to support my work, feel free to drop a tip. Your contribution goes a long way in keeping this knowledge train chugging.

Thanks for reading, and here’s to your continued growth as a Django developer. Stay curious, keep learning, and happy coding! 👋

Ready to level up your programming skills? Become a supporter on my Patreon and unlock the best exclusive content, including step-by-step guides and highly explanatory videos coming soon!

🔒 What’s Included:

  • 📝 Exclusive Articles & Quizzes: Step-by-step guides and interactive exercises.
  • 🎥 In-Depth Videos: Extremely explanatory videos to make complex topics simple. (Soon)
  • 🛠 Code Reviews: Get personalized feedback on your projects.
  • 💡 Fast Q&A: Got questions? Get quick, expert answers.

Subscribe now and be the first to access all these goodies!

My Patreon

In Plain English

Thank you for being a part of our community! Before you go:

Django
Django Framework
Django Girls
Tips And Tricks
Python Django Developer
Recommended from ReadMedium