diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index 6174b7bc98..205a41c193 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -16,6 +16,7 @@ from django.db.models.constants import LOOKUP_SEP
 from django.db.models.query_utils import DeferredAttribute, RegisterLookupMixin
 from django.utils import timezone
 from django.utils.choices import (
+    BlankChoiceIterator,
     CallableChoiceIterator,
     flatten_choices,
     normalize_choices,
@@ -1055,14 +1056,9 @@ class Field(RegisterLookupMixin):
         as <select> choices for this field.
         """
         if self.choices is not None:
-            choices = list(self.choices)
             if include_blank:
-                blank_defined = any(
-                    choice in ("", None) for choice, _ in self.flatchoices
-                )
-                if not blank_defined:
-                    choices = blank_choice + choices
-            return choices
+                return BlankChoiceIterator(self.choices, blank_choice)
+            return self.choices
         rel_model = self.remote_field.model
         limit_choices_to = limit_choices_to or self.get_limit_choices_to()
         choice_func = operator.attrgetter(
diff --git a/django/utils/choices.py b/django/utils/choices.py
index 54dbdcb3aa..7f40bce510 100644
--- a/django/utils/choices.py
+++ b/django/utils/choices.py
@@ -1,10 +1,11 @@
 from collections.abc import Callable, Iterable, Iterator, Mapping
-from itertools import islice, zip_longest
+from itertools import islice, tee, zip_longest
 
 from django.utils.functional import Promise
 
 __all__ = [
     "BaseChoiceIterator",
+    "BlankChoiceIterator",
     "CallableChoiceIterator",
     "flatten_choices",
     "normalize_choices",
@@ -34,6 +35,20 @@ class BaseChoiceIterator:
         )
 
 
+class BlankChoiceIterator(BaseChoiceIterator):
+    """Iterator to lazily inject a blank choice."""
+
+    def __init__(self, choices, blank_choice):
+        self.choices = choices
+        self.blank_choice = blank_choice
+
+    def __iter__(self):
+        choices, other = tee(self.choices)
+        if not any(value in ("", None) for value, _ in flatten_choices(other)):
+            yield from self.blank_choice
+        yield from choices
+
+
 class CallableChoiceIterator(BaseChoiceIterator):
     """Iterator to lazily normalize choices generated by a callable."""
 
diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py
index d1716ce201..43bb770f7e 100644
--- a/tests/model_forms/tests.py
+++ b/tests/model_forms/tests.py
@@ -23,6 +23,7 @@ from django.forms.models import (
 from django.template import Context, Template
 from django.test import SimpleTestCase, TestCase, ignore_warnings, skipUnlessDBFeature
 from django.test.utils import isolate_apps
+from django.utils.choices import BlankChoiceIterator
 from django.utils.deprecation import RemovedInDjango60Warning
 
 from .models import (
@@ -2012,6 +2013,38 @@ class ModelFormBasicTests(TestCase):
             ),
         )
 
+    @isolate_apps("model_forms")
+    def test_callable_choices_are_lazy(self):
+        call_count = 0
+
+        def get_animal_choices():
+            nonlocal call_count
+            call_count += 1
+            return [("LION", "Lion"), ("ZEBRA", "Zebra")]
+
+        class ZooKeeper(models.Model):
+            animal = models.CharField(
+                blank=True,
+                choices=get_animal_choices,
+                max_length=5,
+            )
+
+        class ZooKeeperForm(forms.ModelForm):
+            class Meta:
+                model = ZooKeeper
+                fields = ["animal"]
+
+        self.assertEqual(call_count, 0)
+        form = ZooKeeperForm()
+        self.assertEqual(call_count, 0)
+        self.assertIsInstance(form.fields["animal"].choices, BlankChoiceIterator)
+        self.assertEqual(call_count, 0)
+        self.assertEqual(
+            form.fields["animal"].choices,
+            models.BLANK_CHOICE_DASH + [("LION", "Lion"), ("ZEBRA", "Zebra")],
+        )
+        self.assertEqual(call_count, 1)
+
     def test_recleaning_model_form_instance(self):
         """
         Re-cleaning an instance that was added via a ModelForm shouldn't raise