diff --git a/django/forms/fields.py b/django/forms/fields.py
index c94a564..c6c6ca5 100644
--- a/django/forms/fields.py
+++ b/django/forms/fields.py
@@ -812,12 +812,10 @@ class TypedChoiceField(ChoiceField):
         self.empty_value = kwargs.pop('empty_value', '')
         super(TypedChoiceField, self).__init__(*args, **kwargs)
 
-    def to_python(self, value):
+    def _coerce(self, value):
         """
-        Validates that the value is in self.choices and can be coerced to the
-        right type.
+        Validate that the value can be coerced to the right type (if not empty).
         """
-        value = super(TypedChoiceField, self).to_python(value)
         if value == self.empty_value or value in self.empty_values:
             return self.empty_value
         try:
@@ -830,6 +828,10 @@ class TypedChoiceField(ChoiceField):
             )
         return value
 
+    def clean(self, value):
+        value = super(TypedChoiceField, self).clean(value)
+        return self._coerce(value)
+
 
 class MultipleChoiceField(ChoiceField):
     hidden_widget = MultipleHiddenInput
@@ -879,12 +881,11 @@ class TypedMultipleChoiceField(MultipleChoiceField):
         self.empty_value = kwargs.pop('empty_value', [])
         super(TypedMultipleChoiceField, self).__init__(*args, **kwargs)
 
-    def to_python(self, value):
+    def _coerce(self, value):
         """
         Validates that the values are in self.choices and can be coerced to the
         right type.
         """
-        value = super(TypedMultipleChoiceField, self).to_python(value)
         if value == self.empty_value or value in self.empty_values:
             return self.empty_value
         new_value = []
@@ -899,6 +900,10 @@ class TypedMultipleChoiceField(MultipleChoiceField):
                 )
         return new_value
 
+    def clean(self, value):
+        value = super(TypedMultipleChoiceField, self).clean(value)
+        return self._coerce(value)
+
     def validate(self, value):
         if value != self.empty_value:
             super(TypedMultipleChoiceField, self).validate(value)
diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt
index ef4ed72..476149b 100644
--- a/docs/ref/forms/fields.txt
+++ b/docs/ref/forms/fields.txt
@@ -375,7 +375,9 @@ For each field, we describe the default widget used if you don't specify
 
         A function that takes one argument and returns a coerced value. Examples
         include the built-in ``int``, ``float``, ``bool`` and other types. Defaults
-        to an identity function.
+        to an identity function. Note that coercion happens after input
+        validation, so it is possible to coerce to a value not present in
+        ``choices``.
 
     .. attribute:: empty_value
 
diff --git a/tests/forms_tests/tests/test_fields.py b/tests/forms_tests/tests/test_fields.py
index f02593e..0b1826b 100644
--- a/tests/forms_tests/tests/test_fields.py
+++ b/tests/forms_tests/tests/test_fields.py
@@ -950,6 +950,22 @@ class FieldsTests(SimpleTestCase):
         f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=True)
         self.assertFalse(f._has_changed(None, ''))
 
+    def test_typedchoicefield_special_coerce(self):
+        """
+        Test a coerce function which results in a value not present in choices.
+        Refs #21397.
+        """
+        def coerce_func(val):
+            return Decimal('1.%s' % val)
+
+        f = TypedChoiceField(choices=[(1, "1"), (2, "2")], coerce=coerce_func, required=True)
+        self.assertEqual(Decimal('1.2'), f.clean('2'))
+        self.assertRaisesMessage(ValidationError,
+            "'This field is required.'", f.clean, '')
+        self.assertRaisesMessage(ValidationError,
+            "'Select a valid choice. 3 is not one of the available choices.'",
+            f.clean, '3')
+
     # NullBooleanField ############################################################
 
     def test_nullbooleanfield_1(self):
@@ -1104,6 +1120,23 @@ class FieldsTests(SimpleTestCase):
         f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=True)
         self.assertFalse(f._has_changed(None, ''))
 
+    def test_typedmultiplechoicefield_special_coerce(self):
+        """
+        Test a coerce function which results in a value not present in choices.
+        Refs #21397.
+        """
+        def coerce_func(val):
+            return Decimal('1.%s' % val)
+
+        f = TypedMultipleChoiceField(
+            choices=[(1, "1"), (2, "2")], coerce=coerce_func, required=True)
+        self.assertEqual([Decimal('1.2')], f.clean(['2']))
+        self.assertRaisesMessage(ValidationError,
+            "'This field is required.'", f.clean, [])
+        self.assertRaisesMessage(ValidationError,
+            "'Select a valid choice. 3 is not one of the available choices.'",
+            f.clean, ['3'])
+
    # ComboField ##################################################################
 
     def test_combofield_1(self):
