Khi gọi form.is_valid(), Django chạy validation theo 3 tầng nối tiếp nhau.
Tầng 1 (per-field): chạy to_python() → built-in validator của field (max_length, EmailValidator...) → custom validator trong validators=[...]. Tầng 2 (clean_<fieldname>()): method tự viết để clean 1 field, có quyền raise ValidationError hoặc trả giá trị đã clean qua self.cleaned_data['<field>']. Tầng 3 (clean()): method toàn form, dùng cho ràng buộc liên field (như password và confirm phải khớp), trả về dict cleaned_data.
class SignupForm(forms.Form):
password = forms.CharField(widget=forms.PasswordInput)
password_confirm = forms.CharField(widget=forms.PasswordInput)
def clean_password(self):
pw = self.cleaned_data['password']
if len(pw) < 8:
raise ValidationError('Tối thiểu 8 ký tự')
return pw
def clean(self):
data = super().clean()
if data.get('password') != data.get('password_confirm'):
raise ValidationError({'password_confirm': 'Mật khẩu không khớp'})
return dataLỗi field-level đi vào form.errors['<field>']; lỗi từ clean() không gắn field nào thì đi vào form.non_field_errors().
Trong clean_<field>, nếu field trước đó đã fail validation thì key đó không có trong cleaned_data — dùng .get() thay vì [key] để tránh KeyError.