In Django's FormView and its model subclasses, when do you override get_form_kwargs() rather than get_initial()?
answer
- defaults versus constructor arguments
- bound data wins over initial
- a copy of a class attribute
- extra argument for __init__
basics
~10 sOverride get_initial() to pre-fill default values on the unbound form; override get_form_kwargs() to change the form's constructor arguments, typically to pass extra ones such as the current user, always starting from super().get_form_kwargs().
solid answer
~40 s`get_form()` instantiates the form with `get_form_kwargs()`. The base version returns `initial` (from `get_initial()`), `prefix`, and on POST or PUT also `data=request.POST` and `files=request.FILES`; `ModelFormMixin` adds `instance=self.object`. `get_initial()` only feeds the `initial` dict: defaults shown on GET, ignored for display once the form is bound. So pre-filling a track from a query parameter is `get_initial()`, while giving the form the request user so its `__init__` can limit a choice queryset is `get_form_kwargs()`. Always extend the dict from `super()`; building a fresh dict drops `data` and the form never binds. And never mutate `self.initial` in place: `get_initial()` returns a copy because the class attribute is shared.
code
python · 30 linesfrom django import forms
from django.views.generic.edit import CreateView
from .models import Proposal, Slot
class ProposalForm(forms.ModelForm):
class Meta:
model = Proposal
fields = ['title', 'abstract', 'track', 'slot']
def __init__(self, *args, user=None, **kwargs):
super().__init__(*args, **kwargs)
if user is not None:
self.fields['slot'].queryset = Slot.objects.filter(conference__organisers=user)
class ProposalCreateView(CreateView):
model = Proposal
form_class = ProposalForm
def get_initial(self):
initial = super().get_initial() # a copy, safe to change
initial['track'] = self.request.GET.get('track', '')
return initial
def get_form_kwargs(self):
kwargs = super().get_form_kwargs() # keeps data, files, instance
kwargs['user'] = self.request.user
return kwargsgo deeper
Remember that get_initial() pre-fills fields and get_form_kwargs() builds the form's constructor arguments.
Explain what the base get_form_kwargs() holds: initial, prefix, data and files on POST or PUT, and instance on model views, and why you extend it with super().
Spot the two silent bugs: mutating the shared class-level initial dict, and a hand-built kwargs dict that drops data so every POST fails validation without errors.
Push a convention of forms that declare explicit constructor arguments, so views stay thin and the same form works in tests, the admin and function views.
## Two hooks, two jobs `FormMixin.get_form()` does one thing: `form_class(**self.get_form_kwargs())`. Everything the form receives flows through that dict. `get_initial()` is one input into it. | Hook | Returns | Used for | |---|---|---| | `get_initial()` | a dict of field defaults, by default `self.initial.copy()` | Pre-filling values on the unbound (GET) form | | `get_form_kwargs()` | every keyword argument for the form's constructor | Binding data, the model instance, a prefix, and any extra argument your form accepts | ## What get_form_kwargs() contains The base implementation in `FormMixin` builds: - `initial` from `get_initial()`; - `prefix` from `get_prefix()`; - `data=self.request.POST` and `files=self.request.FILES`, **only when the method is POST or PUT**. `ModelFormMixin` then adds `instance=self.object`, which is `None` on a `CreateView` and the fetched object on an `UpdateView`. That `instance` is what turns a save into an `UPDATE` of the right row. ## When to override get_initial() Initial values are suggestions for an **unbound** form. Once the form is bound to POST data, the submitted values are what render and validate; `initial` is then only used by `has_changed()`. Good uses: - pre-selecting the track from `?track=backend` on the proposal page; - defaulting the talk length to the conference's standard slot; - copying fields from a previous proposal for a "duplicate" action. Override it by updating the copy returned by `super().get_initial()`. The trap is writing `self.initial['track'] = ...`: `initial` is a class attribute holding one dict shared by every request the process serves, so one user's value leaks into everyone else's form. `get_initial()` returns a copy precisely so that overrides stay per-request. ## When to override get_form_kwargs() Override it when the form's constructor needs something the view has and the form does not: 1. Add an `__init__(self, *args, user=None, **kwargs)` to the form that pops the argument before calling `super().__init__`. 2. In the view, take `kwargs = super().get_form_kwargs()`, set `kwargs['user'] = self.request.user`, and return it. 3. In the form, use the argument, for example to restrict a co-speaker or slot queryset to the user's own conference. Forgetting `super()` is a silent bug: a hand-built dict with only `user` in it has no `data`, so the form is never bound, `is_valid()` is always false, and every POST re-renders the page through `form_invalid()` without an error message. ## Other constructor arguments The same hook is where you pass `use_required_attribute`, a custom `error_class`, or `label_suffix`. The `prefix` attribute has its own hook, `get_prefix()`, which is handy when two forms share one page. Anything that is not a constructor argument, such as extra template variables, belongs in `get_context_data()` instead. ## A quick decision rule - The value should appear pre-filled and be editable: `get_initial()`. - The form needs to know something to build its fields or validate: `get_form_kwargs()`. - The value must be set on the saved object and never be editable: neither; set it in `form_valid()`.
- Why does a PATCH request to a Django UpdateView never bind its form?`FormMixin.get_form_kwargs()` only adds `data` and `files` for POST and PUT, and `ProcessFormView` defines no `patch()` handler, so a PATCH is answered with 405 before the form is even built. Browser forms only send GET and POST anyway.
- Is initial data validated when the form is submitted?No. Initial values are only used to render the unbound form and to compute `has_changed()`. Validation runs on the submitted `data`, so a default you put in `initial` can be changed or removed by the user.
saying these in an interview costs you the question
- Set self.initial['track'] in the view to pre-fill the form
- Initial values override whatever the user posted
- get_form_kwargs can return a fresh dict without calling super()
- The request object is automatically passed to every form
- get_initial is the place to pass the current user to the form