Introduction

Picture this: you're three months into a Django project and you realise your nav bar needs to show the number of unread notifications on every single page. Simple enough. You add it to one view. Then another. Then a third. Before long you're copying the same queryset into a dozen different views and your codebase starts looking like a copy-paste accident.

This is the problem context processors solve. Instead of threading data through every view manually, you write it once and Django takes care of the rest. Every template on your site gets the data automatically, without you touching a single view.

What is a context processor?

At its core, a context processor is just a Python function. It receives the current HttpRequest and returns a plain dictionary. Whatever you put in that dictionary shows up as a variable in every template Django renders through render().

That's the whole contract. One argument in, one dictionary out.

Think of context processors as a silent assistant that runs before every template renders. You tell it what to prepare and it quietly makes those things available without anyone needing to ask.

The signature never changes regardless of what the processor does:

python
def my_processor(request):
    return {
        "key": "value",
    }

The simplicity here is intentional. A context processor should do one thing, return a dict, and get out of the way. You will thank yourself for this discipline later.

Built-in processors

When you start a new Django project, several context processors are already wired up for you inside TEMPLATES[0]['OPTIONS']['context_processors'] in your settings. You're already benefiting from them whether you realise it or not.

settings.py
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

Here is what each one quietly adds to every template:

  • debug injects a debug boolean and a sql_queries list when DEBUG=True. Useful for showing debug info in development layouts.
  • request makes the whole request object available so templates can read things like request.user and request.path.
  • auth gives you a user object representing whoever is logged in, plus a perms helper for checking permissions directly in templates.
  • messages provides the messages list so your base template can render flash messages without any view needing to pass them explicitly.

Notice how you write {{ user.username }} or {% if user.is_authenticated %} in your templates without setting those up yourself. That's the auth processor doing its job every single request.

Writing your own

Create a file called context_processors.py inside one of your apps. The convention is to put it alongside your views.py. Each processor is just a function in that file, so you can have as many as you need.

Here are two practical examples to get the idea across. The first exposes some site-wide config so you can reference it from any template. The second injects a cart item count for logged-in users.

myapp/context_processors.py
from django.conf import settings


def site_settings(request):
    """Inject site-wide settings into every template."""
    return {
        "SITE_NAME": settings.SITE_NAME,
        "SITE_URL": settings.SITE_URL,
        "SUPPORT_EMAIL": settings.SUPPORT_EMAIL,
    }


def cart_summary(request):
    """Inject cart item count for authenticated users."""
    if not request.user.is_authenticated:
        return {"cart_count": 0}

    count = request.user.cart_items.filter(active=True).count()
    return {"cart_count": count}

✦ Tip

One processor, one concern

Resist the temptation to build a single processor that injects everything. Splitting them up makes each one easier to test, easier to disable, and much easier to read six months from now.

Registering it

Writing the function is only half of it. Django needs to know the processor exists before it will run it. Open settings.py and add the dotted path to your function inside the context_processors list:

settings.py
TEMPLATES = [
    {
        ...
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
                # your custom processors
                "myapp.context_processors.site_settings",
                "myapp.context_processors.cart_summary",
            ],
        },
    },
]

That's all there is to registering it. From this point on, every template your app renders will have {{ SITE_NAME }} and {{ cart_count }} available without any view needing to pass them in.

base.html
<!-- No view changes needed, these just work -->
<title>{{ SITE_NAME }}</title>

<nav>
  <a href="/">{{ SITE_NAME }}</a>
  {% if user.is_authenticated %}
    <a href="/cart/">Cart ({{ cart_count }})</a>
  {% endif %}
</nav>

Real-world examples

Let's look at three patterns you will reach for regularly once you start building production Django apps. Each one solves a real annoyance you would otherwise work around with copy-pasted view logic.

Feature flags

Say you're rolling out a new dashboard to a subset of users. Rather than checking a flag in every view that might render a link to it, expose the flags through a context processor and check them directly in your templates.

core/context_processors.py
from django.conf import settings


def feature_flags(request):
    """Expose feature flags to all templates."""
    flags = getattr(settings, "FEATURE_FLAGS", {})
    return {"features": flags}
settings.py
FEATURE_FLAGS = {
    "new_dashboard": True,
    "beta_checkout": False,
    "ai_suggestions": True,
}
django
{% if features.new_dashboard %}
  <a href="/dashboard/v2/">Try the new dashboard</a>
{% endif %}

User preferences

If your app supports themes, timezones, or locales, you want those preferences available everywhere without passing them from individual views. A context processor is the right place to load and expose them.

accounts/context_processors.py
def user_preferences(request):
    """Inject user preferences for authenticated users."""
    if not request.user.is_authenticated:
        return {}

    try:
        prefs = request.user.preferences
        return {
            "theme": prefs.theme,        # "light" or "dark"
            "timezone": prefs.timezone,  # "Africa/Nairobi"
            "locale": prefs.locale,      # "en-KE"
        }
    except AttributeError:
        return {
            "theme": "light",
            "timezone": "UTC",
            "locale": "en",
        }

Highlighting the active section in a nav bar usually means passing an active_section variable from every view. A context processor can derive this from the request path instead, so your nav just works.

core/context_processors.py
def navigation(request):
    """Build the main nav and mark the active section."""
    links = [
        {"label": "Dashboard", "url": "/dashboard/", "prefix": "/dashboard"},
        {"label": "Projects",  "url": "/projects/",  "prefix": "/projects"},
        {"label": "Settings",  "url": "/settings/",  "prefix": "/settings"},
    ]

    for link in links:
        link["active"] = request.path.startswith(link["prefix"])

    return {"nav_links": links}

Testing

Because context processors are plain functions, testing them is straightforward. You create a fake request, call the function, and check what came back. There's no need for a running server or a full test client.

tests/test_context_processors.py
from django.test import RequestFactory, TestCase
from django.contrib.auth import get_user_model

from myapp.context_processors import cart_summary, site_settings

User = get_user_model()


class TestSiteSettings(TestCase):
    def test_returns_site_name(self):
        factory = RequestFactory()
        request = factory.get("/")
        ctx = site_settings(request)
        self.assertIn("SITE_NAME", ctx)


class TestCartSummary(TestCase):
    def test_anonymous_user_gets_zero(self):
        factory = RequestFactory()
        request = factory.get("/")
        from django.contrib.auth.models import AnonymousUser
        request.user = AnonymousUser()
        ctx = cart_summary(request)
        self.assertEqual(ctx["cart_count"], 0)

    def test_authenticated_user_gets_count(self):
        user = User.objects.create_user(username="alice", password="pass")
        factory = RequestFactory()
        request = factory.get("/")
        request.user = user
        ctx = cart_summary(request)
        self.assertIn("cart_count", ctx)

⚠ Gotcha

Use RequestFactory, not the test client

The Django test client runs through the full middleware stack. When you're unit-testing a context processor, that overhead is unnecessary and can cause unexpected side effects. RequestFactory creates a bare request with nothing else attached.

Performance

Here's the thing people miss until it's too late: context processors run on every single request. If yours makes a database call without caching it, you've just added a database query to every page load on your site. That adds up quickly on anything with real traffic.

The fix depends on what kind of data you're working with. For site-wide data that changes rarely, use Django's cache framework. For user-specific data that only needs to be fetched once per request, store the result on the request object itself.

core/context_processors.py
from django.core.cache import cache


def global_categories(request):
    """
    Categories change rarely, so we cache them for five minutes.
    This means one database query every 300 seconds instead of
    one on every page load.
    """
    categories = cache.get("global_categories")

    if categories is None:
        from shop.models import Category
        categories = list(
            Category.objects.filter(active=True).values("id", "name", "slug")
        )
        cache.set("global_categories", categories, timeout=300)

    return {"categories": categories}

For per-user data like a cart count, storing the result on the request object works well. The value is computed once and reused if something else in the same request calls the processor again.

python
def cart_summary(request):
    if not request.user.is_authenticated:
        return {"cart_count": 0}

    # Store the result on the request so it is only computed once
    # per request, even if this processor is called multiple times.
    if not hasattr(request, "_cart_count"):
        request._cart_count = request.user.cart_items.filter(active=True).count()

    return {"cart_count": request._cart_count}

Summary

Context processors are one of those Django features that feel obvious once you know them but cause real pain before you do. Here's what to take away.

  • A context processor is a function that takes a request and returns a dictionary. Every key in that dictionary is available in every template.
  • Django ships four by default: debug, request, auth, and messages. You've been using them all along.
  • Register your own in TEMPLATES[0]['OPTIONS']['context_processors'] using the dotted path to your function.
  • Keep each processor focused on one thing. Split them up instead of building one that does everything.
  • Test them with RequestFactory. No middleware, no server, just a function call.
  • Cache anything that touches the database. These functions run on every request, so the cost multiplies fast.