skip to content

In Django, why can't you put a function view decorator like require_POST directly on a class-based view method, and how does method_decorator fix it?

level: middleimportance: must knowfreq 52%

answer

  1. decorators expect request first
  2. a method receives self first
  3. adapter from django.utils.decorators
  4. name="dispatch" on the class
  5. or wrap as_view() in the URLconf

basics

~20 s

Function view decorators expect request as the first argument, but a method receives self first, so the decorator inspects the wrong object. method_decorator adapts the decorator to methods; apply it to dispatch() or to the class with name="dispatch".

solid answer

~40 s

Django's view decorators are written for `view(request, *args, **kwargs)`. On a method the first argument is `self`, so `@require_POST` on `def post(self, request)` ends up reading `self.method` and fails with `AttributeError`. `django.utils.decorators.method_decorator` converts the decorator: it binds the method and applies the decorator to a function that takes `request` first. Use it on a method, or on the class with `@method_decorator(require_POST, name="dispatch")` so it covers every HTTP method. It also accepts a list of decorators, applied in the order listed. `name` must name an existing method or `ValueError` is raised. For one URL only, wrap the result instead: `require_POST(ProductUpdateView.as_view())`. Since 5.2 it also handles `async def` methods.

code

python · 23 lines
python
from django.urls import path
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.http import require_POST
from django.views.decorators.vary import vary_on_cookie


@method_decorator([vary_on_cookie, require_POST], name="dispatch")
class ProductPriceUpdateView(View):
    def post(self, request, pk):
        ...


class ProductPreviewView(View):
    def get(self, request, pk):
        ...


urlpatterns = [
    path("products/<int:pk>/price/", ProductPriceUpdateView.as_view()),
    # decorate one URL only by wrapping the view function as_view() returns
    path("products/<int:pk>/preview/", vary_on_cookie(ProductPreviewView.as_view())),
]

go deeper

for a junior

Remember to wrap function decorators with method_decorator when using class-based views, usually on dispatch.

for a middle

Explain the self-versus-request mismatch, the name="dispatch" class form, list ordering and the URLconf alternative.

for a senior

Choose between method_decorator, per-URL wrapping and the class's own attributes or mixins, and reuse shared decorator lists across views.

for a principal

Set a convention for how cross-cutting view behaviour is attached so function and class-based views stay consistent and reviewable.

## The mismatch A Django **function view decorator** is a function that takes a view and returns a wrapper with the same calling convention: `wrapper(request, *args, **kwargs)`. The wrappers in `django.views.decorators.http`, for example, begin by reading `request.method`. A method on a class-based view is called as `view_instance.post(request, ...)`; inside the class body it is a plain function whose first parameter is `self`. If you decorate it directly, the wrapper's first parameter receives `self`, the view instance: - `require_POST` reads `self.method` and raises `AttributeError`, because the view instance has no `method` attribute; - a decorator that only touches the returned response, such as `vary_on_cookie`, may appear to work by accident, which is worse, because the next decorator you add will not. So the rule is not "it always crashes" but "it is not supported and breaks for any decorator that looks at the request". ## What method_decorator does `django.utils.decorators.method_decorator(decorator, name="")` returns an adapter. When the method is called, the adapter: 1. binds the original method to `self`, producing a callable whose first argument is `request`; 2. applies the function decorator to that bound callable; 3. calls the result with the remaining arguments. The decorator therefore sees exactly the signature it was written for. ## Three ways to apply it | Where | Syntax | Effect | |---|---|---| | On a method | `@method_decorator(require_POST)` above `def dispatch(...)` | every request through `dispatch()` | | On the class | `@method_decorator(require_POST, name="dispatch")` | same, without overriding `dispatch()` | | In the URLconf | `path(..., require_POST(ProductUpdateView.as_view()))` | only this URL pattern | Decorating **`dispatch`** is the usual choice because it is the single entry point before Django routes to `get()`, `post()` and the rest. Decorating only `get()` leaves `post()` unprotected. More details worth knowing: - `decorator` can be a **list or tuple**; they are applied so that they run in the order listed, the first being outermost. Define a list once, for example `product_decorators = [vary_on_cookie, require_safe]`, and reuse it. - With `name`, the attribute must exist and be callable; otherwise `method_decorator` raises `ValueError` or `TypeError` at class creation. - Since **Django 5.2**, `method_decorator` marks the wrapper as a coroutine function when the method is `async def`, so async class-based views can be decorated the same way. ## Errors you will meet - `@method_decorator(require_POST)` on the class **without** `name` raises `ValueError` ("The keyword argument `name` must be the name of a method of the decorated class"). - A misspelt `name` such as `"dispatcher"` raises the same `ValueError`, because the attribute does not exist. - A plain decorator on the class, `@require_POST` above `class ...`, wraps the class object itself, so calling `as_view()` fails or the guard never runs. - Decorating only `get()` and later adding `post()` silently leaves the new handler unprotected; decorating `dispatch()` avoids that drift. A quick test catches most of these: issue a request with a disallowed method and assert the expected status, then issue an allowed one and assert it reaches the handler. ## When you do not need it Some decorator jobs have a class-based equivalent that is more idiomatic: - restricting methods: the view's `http_method_names` attribute already makes `dispatch()` answer other methods with `HttpResponseNotAllowed`; - authentication and permissions: mixins exist for those. `method_decorator` remains the tool for everything that exists only as a function decorator: `condition()`, `vary_on_headers()`, `gzip_page`, `cache_control()`, or your own decorators. ## A worked example For a product-editing view that must only accept `POST` and whose page varies by cookie: ```python @method_decorator([vary_on_cookie, require_POST], name="dispatch") class ProductPriceUpdateView(View): def post(self, request, pk): ... ``` `vary_on_cookie` is outermost, so it runs last on the way out and patches `Vary` on every response, including the 405 produced by `require_POST`.

  • Why decorate dispatch() rather than get() or post()?
    `dispatch()` is the single entry point that routes to the handler for the request's method. A decorator there covers every method, including ones added later. Decorating only `post()` leaves `get()` and any other handler unguarded, which matters for decorators like `require_POST` or `cache_control`.
  • In what order do decorators in @method_decorator([a, b], name="dispatch") run?
    In the order listed: `a` is the outermost wrapper and runs first on the request, `b` runs next, then `dispatch()`. Internally Django reverses the list before wrapping so that the call order matches the list, the same as writing `@a` above `@b`.

saying these in an interview costs you the question

  • Function decorators work on methods because Python passes self automatically
  • method_decorator changes what the decorator does, not just its signature
  • Decorating get() protects every HTTP method of the view
  • method_decorator only accepts a single decorator
  • Decorating as_view() in the URLconf changes the view for every URL that uses the class