Proposal: Incorporating Context-Local Storage in Django

Note: This is my first time writing posting an idea on this forum. Please be kind.


Proposal: Incorporating Context-Local Storage in Django

Introduction

Django, as a leading web framework, currently provides mechanisms like middleware and the request object for passing data throughout the application. However, these solutions have limitations, especially when dealing with code that requires access to context-specific data without tight coupling to the request object.

Background

Python 3.7 introduced the contextvars module (PEP 567), which allows storage of context-local data that is safe to use in concurrent code. This feature enables developers to store data that is local to a particular execution context, such as an individual web request, without the risk of data leakage between threads or requests.

Proposal

It is proposed that Django should include a built-in application context module that provides context-local storage using contextvars. This module would offer:

  • A standardized way to store and access context-local variables: Developers can store data specific to the current request without worrying about data leakage across threads.
  • Cleaner codebases: By avoiding passing context-specific data through multiple function parameters or relying on global variables, code becomes more maintainable and readable.

Limitations in Current Django Implementation

Passing Data via Middleware and Request Object

In Django, it’s common to pass data through middleware by attaching it to the request object. While effective, this approach introduces Increased Coupling. Functions and utilities that require access to request-specific data need the data to be drilled down through each layer, tightly coupling the overall application to the field being passed down.

Benefits of Context-Local Storage

  1. Concurrency Safety: Context-local storage ensures that data remains isolated within its execution context, preventing accidental sharing between concurrent requests.
  2. Cleaner Codebase: Developers can avoid passing the request object through multiple layers, leading to more maintainable and testable code.
  3. Decoupling: Utilities and services can access context-specific data without direct dependencies on the request object.

A Solution

  • Context Store Class: Introduce a class (e.g., ApplicationContextStore) that encapsulates context-local variables using contextvars.ContextVar.
  • Context Manager: Provide a context manager (e.g., application_context_var_manager) to set and reset context variables within a specific execution scope.
  • Example Usage: Integration with Django’s Request/Response Cycle
    • Set context-local variables at the beginning of a request.
    • Automatically clean up these variables after the request is completed.

Implementation Example

Defining Context Variables

# application_context.py

from contextlib import contextmanager
from contextvars import ContextVar

class ApplicationContextStore:
    """Context store for managing context-local variables."""
    tenant_id = ContextVar('tenant')

# Instantiate the class and export it through a variable
application_context_store = ApplicationContextStore()

@contextmanager
def application_context_var_manager(contextvar, value):
    """Context manager for setting a contextvar."""
    token = contextvar.set(value)
    try:
        yield
    finally:
        contextvar.reset(token)

Setting Context Variables in Middleware

# middleware.py

from .application_context import application_context_store, application_context_var_manager

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

    def _get_tenant_id_from_request(self, request):
        """Logic to determine tenant_id from request"""
        ... # Implementation details left out

    def __call__(self, request):
        tenant_id = self._get_tenant_from_request(request)
        # Set the tenant_id in the contextvar
        with application_context_var_manager(application_context_store.tenant_id, tenant_id):
            response = self.get_response(request)
        return response

Accessing Context Variables in Settings

# settings.py

from .application_context import application_context_store
import logging

# Custom filter that adds tenant_id ID to the log record
class TenantIdFilter(logging.Filter):
    def filter(self, record):
        try:
            tenant_id = application_context_store.tenant_id.get()
        except LookupError:
            tenant_id = 'Unknown Tenant'
        record.tenant_id = tenant_id
        return True

# Apply the filter
LOGGING = {
    'version': 1,
    # ...
    'filters': {
        'tenant_id_filter': {
            '()': TenantIdFilter,
        },
    },
    'formatters': {
        'standard': {
            'format': '%(asctime)s [%(levelname)s] %(name)s [tenant_id ID: %(tenant_id)s]: %(message)s'
        },
    },
    'handlers': {
        'console': {
            'filters': ['tenant_id_filter'],
            # ...
        },
    },
    # ...
}

Conclusion

Incorporating context-local storage into Django’s core would enhance its ability to handle context-specific data safely and efficiently. By providing a standardized way to manage execution context-specific data, Django would empower developers to write cleaner, more maintainable code.

Hey

Thanks for your proposal. I’m not 100% sure what exactly you’re proposing, since the contextvars module exists, is a part of the Python ecosystem and can be used easily (as you’ve demonstrated) in Django code. What utilities should be added to Django, and how would using them be different from using contextvars directly?

[…] especially when dealing with code that requires access to context-specific data without tight coupling to the request object.

You’re of course correct, passing the request everywhere can be annoying. On the other hand, having to pass the request object explicitly makes it immediately clear that the behavior of functions is dependent upon the current request. Using something like contextvars, or the Local class from threading or asgiref doesn’t change that, it only hides it – and hiding can actually be worse in some scenarios.

[…] such as an individual web request, without the risk of data leakage between threads or requests.

If you pass the request object explicitly you’re at least aware that the behavior depends on the request. If you’re using contextvars or something it’s up to you to explicitly reset the value when the request ends. The application_context_var_manager makes it easier to do the right thing.

I think a HOWTO covering this topic could be very useful. I am not convinced the utilities are all that helpful, but that’s just me. Please don’t let it discourage you.