Skip to content

forms & forms_meta

Two static-ish checks on every first-party Form / ModelForm — no HTTP, no .save(), fast.

forms

Walks every app's modules, collects every forms.BaseForm subclass, and instantiates each one twice:

  1. FormCls() — the blank GET path a Django view uses to render the form.
  2. FormCls(data={}) — the empty-POST path.

Any exception becomes an ERROR finding at forms.blank_init / forms.empty_data.

Forms that legitimately require kwargs

Django ships SetPasswordForm(user, …), AuthenticationForm(request, …), and many codebases layer their own. A TypeError matching missing N required … arguments is downgraded to INFO with rule forms.blank_init.needs_args — don't fail CI on them.

Real bug this caught on altiusone

class ContactPrincipalForm(forms.ModelForm):
    class Meta:
        model = Contact
        fields = ["fonction", ...]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["fonction"].choices = [...]   # AttributeError

Contact.fonction is a bare CharField, no choices=. The form raised AttributeError on every GET. ./manage.py check passed. The view test passed because it prepared the form with valid data. Doctor caught it in half a second.

forms_meta

Static AST analysis, no instantiation. Finds the anti-pattern:

class OperationTVAForm(forms.ModelForm):
    class Meta:
        model = OperationTVA
        fields = ["montant_ht", "taux_tva", ...]    # no montant_ttc

    def clean(self):
        cleaned_data = super().clean()
        cleaned_data["montant_ttc"] = ...           # orphan!
        return cleaned_data

The flow:

  1. AST-parse each ModelForm's source.
  2. Collect every cleaned_data[<str>] = … inside clean() / clean_*().
  3. For each key:
    • Skip if it's in Meta.fields or a declared form field.
    • Skip if the form defines a save() override (author handles it).
    • Skip if the model field is nullable or has a default.
    • Otherwise → ERROR.

Finding shape

[ERROR:forms.cleaned_data.orphan]
  myapp.forms.OperationTVAForm:67
  cleaned_data['montant_ttc'] is assigned in clean() but 'montant_ttc'
  is not in Meta.fields; OperationTVA.montant_ttc is NOT NULL without
  default. form.save() will silently drop the value and raise
  IntegrityError.

Fixes

Two canonical fixes the check is happy with:

class Meta:
    model = OperationTVA
    fields = ["montant_ht", "taux_tva", "montant_ttc"]
    widgets = {"montant_ttc": forms.HiddenInput()}
def save(self, commit=True):
    instance = super().save(commit=False)
    instance.montant_ttc = self.cleaned_data["montant_ttc"]
    if commit:
        instance.save()
        self.save_m2m()
    return instance