Based on my knowledge of Django's codebase, I can analyze and fix this issue. The problem is in `django/forms/models.py` in the `apply_limit_choices_to_to_formfield` function or more specifically in how the form field's `show_hidden_initial` is determined. The issue is: 1. When a model field has a callable default, Django's form field gets `show_hidden_initial=True` 2. This is because the callable is evaluated, making it appear the field has an initial value that could change 3. However, for fields with callable defaults that return empty/default values (like `list`), this causes issues with `has_changed()` detection Looking at Django's code, the fix should be in `django/forms/models.py` where `show_hidden_initial` is set based on whether the field has a callable default. The specific issue is that when the default is callable, the form field shouldn't automatically get `show_hidden_initial=True` just because the initial value was derived from a callable. The fix should be in the function that constructs form fields from model fields. The `show_hidden_initial` should not be set to `True` when the initial value comes from a callable default on the model field. Here's the patch: --- a/django/forms/models.py +++ b/django/forms/models.py @@ -92,7 +92,10 @@ def construct_instance(form, instance, fields=None, exclude=None): def apply_limit_choices_to_to_formfield(formfield): """Apply limit_choices_to to the formfield's queryset if needed.""" from django.db.models import Exists, OuterRef, Q - if hasattr(formfield, 'queryset') and hasattr(formfield, 'get_limit_choices_to'): + if hasattr(formfield, "queryset") and hasattr(formfield, "get_limit_choices_to"): limit_choices_to = formfield.get_limit_choices_to() if limit_choices_to: complex_filter = limit_choices_to @@ -188,6 +191,10 @@ class ModelForm(BaseForm, metaclass=ModelFormMetaclass): def modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None, localized_fields=None, labels=None, help_texts=None, error_messages=None): + """ + Return a ModelForm containing form fields for the given model. You can + optionally pass a `form` argument to use as a starting point for + constructing the ModelForm. --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -136,8 +136,10 @@ class Field: def has_changed(self, initial, data): """Return True if data differs from initial.""" # Always return False if the field is disabled since self.bound_data - # always uses the initial value in this case. + # always uses the initial value in this case. if self.disabled: return False try: data = self.to_python(data) Let me reconsider the actual fix based on the issue description more carefully. The issue is specifically about: 1. A field with `default=list` (callable) 2. On first submit with error, the hidden initial value gets the user's input 3. On second submit, validation is bypassed The root cause is that `show_hidden_initial` is being set to `True` for fields with callable defaults. The fix should ensure that when a form field's initial value is derived from a callable default on a model field, `show_hidden_initial` should be set based on whether the callable actually produces a value that could change between requests. Looking at the Django code flow: 1. `modelform_defines_fields` checks fields 2. `fields_for_model` creates form fields from model fields 3. For each field, `formfield()` is called which sets `show_hidden_initial` The fix should be in how `show_hidden_initial` is determined. When the initial value comes from evaluating a callable default, the resulting value should be used for the hidden initial comparison, not the fact that it came from a callable. --- a/django/forms/models.py +++ b/django/forms/models.py @@ -43,6 +43,9 @@ def construct_instance(form, instance, fields=None, exclude=None): def model_to_dict(instance, fields=None, exclude=None): """ Return a dict containing the data in ``instance`` suitable for passing as @@ -86,7 +89,14 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_callback=None, localized_fields=None, labels=None, help_texts=None, error_messages=None, field_classes=None, *, apply_limit_choices_to=True): - """ + """Return a dictionary containing form fields for the given model. + + ``fields`` is an optional list of field names. If provided, return only the + named fields. + + ``exclude`` is an optional list of field names. If provided, exclude the + named fields from the returned fields, even if they are listed in the + ``fields`` argument. field_dict = {} ignored = [] opts = model._meta Based on the issue, the actual fix needs to be in how the initial value is propagated when a callable default is involved. The problem is in `django/forms/models.py` in the function that creates form fields. --- a/django/forms/models.py +++ b/django/forms/models.py @@ -92,6 +92,9 @@ def fields_for_model( continue kwargs = {} + if form_field is not None and f.has_default() and callable(f.default): + kwargs["show_hidden_initial"] = False if widgets and f.name in widgets: kwargs["widget"] = widgets[f.name] if localized_fields == ALL_FIELDS or ( Wait, I need to be more precise about the location of the fix. Let me provide the correct patch based on Django's actual codebase structure: --- a/django/forms/models.py +++ b/django/forms/models.py @@ -47,6 +47,12 @@ __all__ = ( ALL_FIELDS = "__all__" +def _get_default_value(field): + """Return the default value for a model field, calling it if callable.""" + if callable(field.default): + return field.default() + return field.default + + def construct_instance(form, instance, fields=None, exclude=None): """ Construct and return a model instance from the bound ``form``'s @@ -91,6 +97,10 @@ def fields_for_model( kwargs = {} if widgets and f.name in widgets: kwargs["widget"] = widgets[f.name] + # Don't show hidden initial for fields with callable defaults, + # as the value has already been evaluated. + if f.has_default() and callable(f.default): + kwargs["show_hidden_initial"] = False if localized_fields == ALL_FIELDS or ( localized_fields and f.name in localized_fields ): Actually, looking at this more carefully, the real issue is about how form fields determine `show_hidden_initial`. The issue is that when a model field has a callable default, the form field's `show_hidden_initial` becomes True, which causes issues with the `has_changed()` detection. Let me provide the correct minimal patch: --- a/django/forms/models.py +++ b/django/forms/models.py @@ -93,6 +93,9 @@ def fields_for_model( kwargs = {} if widgets and f.name in widgets: kwargs["widget"] = widgets[f.name] + # Fields with callable defaults should not show hidden initial. + if f.has_default() and callable(f.default): + kwargs["show_hidden_initial"] = False if localized_fields == ALL_FIELDS or ( localized_fields and f.name in localized_fields ):