diff --git a/django/forms/forms.py b/django/forms/forms.py
--- a/django/forms/forms.py
+++ b/django/forms/forms.py
@@ -253,16 +253,17 @@
     def full_clean(self):
         """
         Cleans all of self.data and populates self._errors and
-        self.cleaned_data.
+        self.cleaned_data. Returns True if validation was performed,
+        False otherwise.
         """
         self._errors = ErrorDict()
         if not self.is_bound: # Stop further processing.
-            return
+            return False
         self.cleaned_data = {}
         # If the form is permitted to be empty, and none of the form data has
         # changed from the initial data, short circuit any validation.
         if self.empty_permitted and not self.has_changed():
-            return
+            return False
         for name, field in self.fields.items():
             # value_from_datadict() gets the data from the data dictionaries.
             # Each widget type knows how to retrieve its own data, because some
@@ -288,6 +289,7 @@
             self._errors[NON_FIELD_ERRORS] = self.error_class(e.messages)
         if self._errors:
             delattr(self, 'cleaned_data')
+        return True
 
     def clean(self):
         """
diff --git a/django/forms/models.py b/django/forms/models.py
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -280,7 +280,15 @@
                     exclude.append(f.name)
         return exclude
 
-    def clean(self):
+    def full_clean(self):
+        needs_validation = super(BaseModelForm, self).full_clean()
+        if needs_validation:
+            self.clean_instance()
+        return needs_validation
+
+    def clean_instance(self):
+        if not hasattr(self, 'cleaned_data'):
+            self.cleaned_data = {}
         opts = self._meta
         self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude)
         exclude = self._get_validation_exclusions()
@@ -288,14 +296,9 @@
             self.instance.full_clean(exclude=exclude)
         except ValidationError, e:
             for k, v in e.message_dict.items():
-                if k != NON_FIELD_ERRORS:
-                    self._errors.setdefault(k, ErrorList()).extend(v)
-                    # Remove the data from the cleaned_data dict since it was invalid
-                    if k in self.cleaned_data:
-                        del self.cleaned_data[k]
-            if NON_FIELD_ERRORS in e.message_dict:
-                raise ValidationError(e.message_dict[NON_FIELD_ERRORS])
-        return self.cleaned_data
+                self._errors.setdefault(k, ErrorList()).extend(v)
+        if self._errors:
+            delattr(self, 'cleaned_data')
 
     def save(self, commit=True):
         """
diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt
--- a/docs/topics/forms/modelforms.txt
+++ b/docs/topics/forms/modelforms.txt
@@ -464,15 +464,6 @@
 You can override the ``clean()`` method on a model form to provide additional
 validation in the same way you can on a normal form.
 
-In this regard, model forms have two specific characteristics when compared to
-forms:
-
-By default the ``clean()`` method validates the uniqueness of fields that are
-marked as ``unique``, ``unique_together`` or ``unique_for_date|month|year`` on
-the model.  Therefore, if you would like to override the ``clean()`` method and
-maintain the default validation, you must call the parent class's ``clean()``
-method.
-
 Also, a model form instance bound to a model object will contain a
 ``self.instance`` attribute that gives model form methods access to that
 specific model instance.
diff --git a/tests/regressiontests/model_forms_regress/tests.py b/tests/regressiontests/model_forms_regress/tests.py
--- a/tests/regressiontests/model_forms_regress/tests.py
+++ b/tests/regressiontests/model_forms_regress/tests.py
@@ -50,6 +50,28 @@
         form = TripleForm({'left': '1', 'middle': '3', 'right': '1'})
         self.failUnless(form.is_valid())
 
+
+class TripleFormWithCleanOverride(forms.ModelForm):
+    class Meta:
+        model = Triple
+
+    def clean(self):
+        if not self.cleaned_data['left'] == self.cleaned_data['right']:
+            raise forms.ValidationError('Left and right should be equal')
+        return self.cleaned_data
+
+class OverrideCleanTests(TestCase):
+    def test_override_clean(self):
+        """
+        Regression for #12596: ModelForm.clean() should be optional.
+
+        """
+        form = TripleFormWithCleanOverride({'left': 1, 'middle': 2, 'right': 1})
+        self.failUnless(form.is_valid())
+        self.assertEquals(form.instance.left, 1,
+                          'form.instance not set up by form.is_valid()')
+
+
 class FPForm(forms.ModelForm):
     class Meta:
         model = FilePathModel
@@ -113,3 +135,4 @@
             date_published=date(1991, 8, 22))
         f = Form()
         self.assertEqual(len(f.fields["publications"].choices), 1)
+
