diff --git a/djblets/conditions/values.py b/djblets/conditions/values.py
index 40e5ee0351b5e16fecc168e021f589b8e9838b77..8c1872ac12949d239c774be36b41693031fcfd85 100644
--- a/djblets/conditions/values.py
+++ b/djblets/conditions/values.py
@@ -320,6 +320,15 @@ class ConditionValueFormField(BaseConditionValueField[_T]):
     ) -> None:
         self._field = field
 
+    @property
+    def widget(self) -> forms.Field:
+        """The associated form field's widget.
+
+        Version Added:
+            5.3
+        """
+        return self.field.widget
+
     def serialize_value(
         self,
         value: _T,
diff --git a/djblets/forms/fields.py b/djblets/forms/fields.py
index b27fd1549df002e70a149a72d6d6d7d521ce5ab5..f9f0a06d10f8c3b7ba02bbb791471a43b415df99 100644
--- a/djblets/forms/fields.py
+++ b/djblets/forms/fields.py
@@ -2,23 +2,26 @@
 
 from __future__ import annotations
 
-import logging
-from typing import Any, Callable, Optional, TYPE_CHECKING, Union
+import copy
+from typing import Any, Optional, TYPE_CHECKING, Union
 
 import pytz
 from django import forms
 from django.utils.safestring import mark_safe
 from django.utils.translation import gettext_lazy as _, gettext
+from typing_extensions import Self, TypeAlias
 
+from djblets.conditions.choices import ConditionChoices
 from djblets.conditions.conditions import ConditionSet
 from djblets.conditions.errors import (ConditionChoiceNotFoundError,
                                        ConditionOperatorNotFoundError,
                                        InvalidConditionModeError,
                                        InvalidConditionValueError)
-from djblets.forms.widgets import ConditionsWidget, ListEditWidget
+from djblets.forms.widgets import (ConditionsWidget,
+                                   ConditionsWidgetChoices,
+                                   ListEditWidget)
 
 if TYPE_CHECKING:
-    from djblets.conditions.choices import ConditionChoices
     from djblets.conditions.conditions import ConditionSetData
     from djblets.util.typing import KwargsDict
 
@@ -26,6 +29,13 @@ if TYPE_CHECKING:
 TIMEZONE_CHOICES = tuple(zip(pytz.common_timezones, pytz.common_timezones))
 
 
+#: The types of arguments available as choices to a ConditionsField.
+#:
+#: Version Added:
+#:     5.3
+ConditionsFieldChoices: TypeAlias = ConditionsWidgetChoices
+
+
 class TimeZoneField(forms.ChoiceField):
     """A form field that only allows pytz common timezones as the choices."""
 
@@ -37,9 +47,18 @@ class ConditionsField(forms.Field):
     """A form field for customizing conditions required for an operation.
 
     This field is used for defining conditions under which something should
-    be processed or run. It takes a condition choice registry (an instance or
-    subclass of :py:class:`~djblets.conditions.choices.ConditionChoices`),
-    which defines all the possible condition choices, each containing possible
+    be processed or run. It takes a condition choice registry, which may be
+    one of the following:
+
+    1. A :py:class:`~djblets.conditions.choices.ConditionChoices` instance
+       (recommended)
+
+    2. A :py:class:`~djblets.conditions.choices.ConditionChoices` subclass
+
+    2. A callable returning a
+       :py:class:`~djblets.conditions.choices.ConditionChoices` instance
+
+    These define all the possible condition choices, each containing possible
     operators for that choice (such as "is," "is not," "starts with," etc.) and
     possible fields for inputting values.
 
@@ -63,9 +82,6 @@ class ConditionsField(forms.Field):
     # Instance variables #
     ######################
 
-    #: The condition choies for the field.
-    choices: ConditionChoices
-
     #: The field for selecting a choice.
     choice_field: forms.ChoiceField
 
@@ -77,9 +93,7 @@ class ConditionsField(forms.Field):
 
     def __init__(
         self,
-        choices: Union[type[ConditionChoices],
-                       ConditionChoices,
-                       Callable[[], ConditionChoices]],
+        choices: ConditionsFieldChoices,
         choice_kwargs: Optional[KwargsDict] = None,
         *args,
         **kwargs,
@@ -116,23 +130,8 @@ class ConditionsField(forms.Field):
             **kwargs (dict):
                 Extra keyword arguments for the field.
         """
-        if callable(choices):
-            choices = choices()
-
-        for choice in choices:
-            if choice.operators is None or len(choice.operators) == 0:
-                logging.error('%s must define a non-empty "operators" '
-                              'attribute.',
-                              choice.__name__)
-
-                raise ValueError(
-                    _('%s must define a non-empty "operators" attribute.')
-                    % choice.__name__)
-
         widget_cls = kwargs.get('widget', self.widget)
 
-        self.choices = choices
-
         self.mode_field = forms.ChoiceField(
             required=True,
             choices=(
@@ -147,9 +146,9 @@ class ConditionsField(forms.Field):
 
         self.choice_field = forms.ChoiceField(
             required=True,
-            choices=(
+            choices=lambda: (
                 (choice.choice_id, choice.name)
-                for choice in choices
+                for choice in self.choices
             ))
 
         self.operator_field = forms.ChoiceField(required=True)
@@ -162,6 +161,29 @@ class ConditionsField(forms.Field):
                               choice_kwargs=choice_kwargs or {}),
             *args, **kwargs)
 
+    @property
+    def choices(self) -> ConditionChoices:
+        """The condition choices for the field.
+
+        Type:
+            djblets.conditions.choices.ConditionChoices
+        """
+        return self.widget.choices
+
+    @choices.setter
+    def choices(
+        self,
+        value: ConditionsFieldChoices,
+    ) -> None:
+        """The condition choices for the field.
+
+        Args:
+            value (djblets.conditions.choices.ConditionChoices or type or
+                   callable):
+                The new choices value.
+        """
+        self.widget.choices = value
+
     @property
     def choice_kwargs(self) -> KwargsDict:
         """The keyword arguments passed to ConditionChoice functions.
@@ -284,6 +306,34 @@ class ConditionsField(forms.Field):
 
         return condition_set
 
+    def __deepcopy__(
+        self,
+        memo: dict[int, Any],
+    ) -> Self:
+        """Return a deep copy of the field.
+
+        This will return a deep copy of this field and all subfields, so that
+        a particular form instance can easily manipulate state without
+        affecting other instances.
+
+        Only the state that may be manipulated by an instance will be deep
+        copied.
+
+        Args:
+            memo (dict):
+                The memo dictionary used to track IDs to objects.
+
+        Returns:
+            ConditionsField:
+            A deep copy of this field's instance.
+        """
+        obj = super().__deepcopy__(memo)
+        obj.choice_field = copy.deepcopy(self.choice_field, memo)
+        obj.mode_field = copy.deepcopy(self.mode_field, memo)
+        obj.operator_field = copy.deepcopy(self.operator_field, memo)
+
+        return obj
+
 
 class ListEditField(forms.Field):
     """A form field for customizing a string representing a list of values.
diff --git a/djblets/forms/tests/test_conditions_field.py b/djblets/forms/tests/test_conditions_field.py
index f45a717ee21a972538c10dd81fb6732e0b2e8131..78d48453ab706b0f4a8711a7d5bf33c0c3aafb11 100644
--- a/djblets/forms/tests/test_conditions_field.py
+++ b/djblets/forms/tests/test_conditions_field.py
@@ -35,19 +35,6 @@ class ConditionsFieldTests(TestCase):
 
         self.assertEqual(field.widget.choice_kwargs, field.choice_kwargs)
 
-    def test_init_with_missing_operators(self):
-        """Testing ConditionsField initialization with choices missing
-        operators
-        """
-        class MyChoice(BaseConditionChoice):
-            choice_id = 'my-choice'
-
-        choices = ConditionChoices([MyChoice])
-        msg = 'MyChoice must define a non-empty "operators" attribute.'
-
-        with self.assertRaisesMessage(ValueError, msg):
-            ConditionsField(choices=choices)
-
     def test_choice_kwargs_with_multiple_instances(self):
         """Testing ConditionsField.choice_kwargs works with multiple instances
         """
diff --git a/djblets/forms/tests/test_conditions_widget.py b/djblets/forms/tests/test_conditions_widget.py
index 94fc8b5fa975ac798943b7dcaf06bbf76661b9b1..f335331888cb7e35bd60f0cd412e7d592d23b7a3 100644
--- a/djblets/forms/tests/test_conditions_widget.py
+++ b/djblets/forms/tests/test_conditions_widget.py
@@ -1,13 +1,18 @@
+from __future__ import annotations
+
 import copy
 
+from django import forms
 from django.utils.datastructures import MultiValueDict
 
 from djblets.conditions.choices import (BaseConditionChoice,
                                         BaseConditionIntegerChoice,
                                         ConditionChoices)
-from djblets.conditions.operators import (BaseConditionOperator,
+from djblets.conditions.operators import (AnyOperator,
+                                          BaseConditionOperator,
                                           ConditionOperators)
 from djblets.conditions.values import (ConditionValueCharField,
+                                       ConditionValueFormField,
                                        ConditionValueIntegerField)
 from djblets.forms.fields import ConditionsField
 from djblets.testing.testcases import TestCase
@@ -35,6 +40,8 @@ class ConditionsWidgetTests(TestCase):
         self.assertEqual(widget2.choice_widget.attrs, {})
         self.assertEqual(widget2.operator_widget.attrs, {})
         self.assertEqual(widget2.condition_errors, {})
+        self.assertIs(widget2._orig_choices, choices)
+        self.assertIsNone(widget2._choices)
 
         # Choices won't be modified between copies, and is a shared object,
         # so both should have the same instance.
@@ -658,3 +665,101 @@ class ConditionsWidgetTests(TestCase):
             '<option value="my-op" selected(="selected")?>My Op</option>')
         self.assertIn('<span class="conditions-field-value"></span>',
                       rendered)
+
+    def test_choices_get_with_class(self) -> None:
+        """Testing ConditionsWidget.choices getter with ConditionSet subclass
+        """
+        class MyChoice(BaseConditionIntegerChoice):
+            choice_id = 'my-choice'
+
+        class MyConditionChoices(ConditionChoices):
+            choice_classes = [
+                MyChoice,
+            ]
+
+        widget = ConditionsField(choices=MyConditionChoices).widget
+
+        self.assertIsInstance(widget.choices, MyConditionChoices)
+        self.assertIs(widget._orig_choices, MyConditionChoices)
+
+    def test_choices_get_with_instance(self) -> None:
+        """Testing ConditionsWidget.choices getter with ConditionSet instance
+        """
+        class MyChoice(BaseConditionIntegerChoice):
+            choice_id = 'my-choice'
+
+        choices = ConditionChoices(choices=[MyChoice])
+        widget = ConditionsField(choices=choices).widget
+
+        self.assertIs(widget.choices, choices)
+        self.assertIs(widget._orig_choices, choices)
+        self.assertIs(widget._choices, choices)
+
+    def test_choices_get_with_callable(self) -> None:
+        """Testing ConditionsWidget.choices getter with callable"""
+        class MyChoice(BaseConditionIntegerChoice):
+            choice_id = 'my-choice'
+
+        choices = ConditionChoices(choices=[MyChoice])
+
+        def _get_choices() -> ConditionChoices:
+            return choices
+
+        widget = ConditionsField(choices=_get_choices).widget
+
+        self.assertIs(widget.choices, choices)
+        self.assertIs(widget._orig_choices, _get_choices)
+        self.assertIs(widget._choices, choices)
+
+    def test_choices_set(self) -> None:
+        """Testing ConditionsWidget.choices setter"""
+        class MyChoice(BaseConditionIntegerChoice):
+            choice_id = 'my-choice'
+
+        choices = ConditionChoices(choices=[MyChoice])
+        widget = ConditionsField(choices=ConditionChoices).widget
+
+        widget.choices = choices
+        self.assertIs(widget.choices, choices)
+        self.assertIs(widget._orig_choices, choices)
+        self.assertIs(widget._choices, choices)
+
+    def test_dynamic_condition_choices(self) -> None:
+        """Testing ConditionsWidget sees new condition choices dynamically
+        added to ConditionChoices
+        """
+        class MyWidget1(forms.widgets.Widget):
+            class Media:
+                js = ['widget1.js']
+
+        class MyWidget2(forms.widgets.Widget):
+            class Media:
+                js = ['widget2.js']
+
+        class MyChoice1(BaseConditionChoice):
+            choice_id = 'my-choice1'
+            default_value_field = ConditionValueFormField(
+                field=forms.CharField(widget=MyWidget1))
+            operators = ConditionOperators([AnyOperator])
+
+        class MyChoice2(BaseConditionIntegerChoice):
+            choice_id = 'my-choice2'
+            default_value_field = ConditionValueFormField(
+                field=forms.CharField(widget=MyWidget2))
+            operators = ConditionOperators([AnyOperator])
+
+        choices = ConditionChoices(choices=[MyChoice1])
+        widget = ConditionsField(choices=choices).widget
+
+        # We'll just test one usage of choices. This is the simplest test,
+        # but will ensure we see changes to the available choices.
+        media = widget.media
+        self.assertEqual(media._css, {})
+        self.assertEqual(media._js, ['widget1.js'])
+
+        # Now add a new choice.
+        choices.register(MyChoice2)
+
+        media = widget.media
+        self.assertEqual(media._css, {})
+        self.assertEqual(media._js, ['widget1.js', 'widget2.js'])
diff --git a/djblets/forms/widgets.py b/djblets/forms/widgets.py
index e7b0f5145abb4aa33a2664ebb3f3cbb28fefe5a7..2b27b480b5a1c536f617835f9a829f21f41d60d8 100644
--- a/djblets/forms/widgets.py
+++ b/djblets/forms/widgets.py
@@ -9,7 +9,7 @@ from __future__ import annotations
 import copy
 from contextlib import contextmanager
 from typing import (Any, Callable, Dict, Iterable, Iterator, List, Optional,
-                    Self, TYPE_CHECKING, Tuple, Union)
+                    TYPE_CHECKING, Tuple, Union)
 
 from django.forms import widgets
 from django.forms.widgets import HiddenInput
@@ -17,8 +17,10 @@ from django.template.loader import render_to_string
 from django.utils.safestring import mark_safe
 from django.utils.translation import gettext as _
 from housekeeping import deprecate_non_keyword_only_args
+from typing_extensions import Self, TypeAlias
 
 from djblets.conditions import ConditionSet
+from djblets.conditions.choices import ConditionChoices
 from djblets.conditions.errors import (ConditionChoiceNotFoundError,
                                        ConditionOperatorNotFoundError)
 from djblets.deprecation import RemovedInDjblets70Warning
@@ -27,14 +29,23 @@ if TYPE_CHECKING:
     from django.forms.renderers import BaseRenderer
     from django.forms.utils import _DataT, _FilesT
     from django.utils.safestring import SafeText
-    from djblets.conditions.choices import (BaseConditionChoice,
-                                            ConditionChoices)
+    from djblets.conditions.choices import BaseConditionChoice
     from djblets.conditions.conditions import ConditionData, ConditionSetData
     from djblets.conditions.operators import BaseConditionOperator
     from djblets.conditions.values import BaseConditionValueField
     from djblets.util.typing import KwargsDict, StrOrPromise
 
 
+#: The types of arguments available as choices to a ConditionsField.
+#:
+#: Version Added:
+#:     5.3
+ConditionsWidgetChoices: TypeAlias = Union[
+    ConditionChoices,
+    Callable[[], ConditionChoices],
+]
+
+
 class AmountSelectorWidget(widgets.MultiWidget):
     """A widget for editing an amount and its unit of measurement.
 
@@ -193,9 +204,6 @@ class ConditionsWidget(widgets.Widget):
     # Instance variables #
     ######################
 
-    #: The condition choices for the field.
-    choices: ConditionChoices
-
     #: Optional keyword arguments to pass to each choice.
     #:
     #: This is useful for more advanced conditions that need additional data
@@ -220,11 +228,23 @@ class ConditionsWidget(widgets.Widget):
     #: One of these will be rendered for every row.
     operator_widget: widgets.Select
 
+    #: The calculated condition choices for the widget.
+    #:
+    #: Version Added:
+    #:     5.3
+    _choices: Optional[ConditionChoices]
+
+    #: The original condition choices passed to the widget.
+    #:
+    #: Version Added:
+    #:     5.3
+    _orig_choices: ConditionsWidgetChoices
+
     @deprecate_non_keyword_only_args(RemovedInDjblets70Warning)
     def __init__(
         self,
         *,
-        choices: ConditionChoices,
+        choices: ConditionsWidgetChoices,
         mode_widget: widgets.RadioSelect,
         choice_widget: widgets.Select,
         operator_widget: widgets.Select,
@@ -233,10 +253,21 @@ class ConditionsWidget(widgets.Widget):
     ) -> None:
         """Initialize the widget.
 
+        Version Changed:
+            5.3:
+            ``choices`` may now be a callable.
+
         Args:
-            choices (djblets.conditions.choices.ConditionChoices):
+            choices (djblets.conditions.choices.ConditionChoices or callable):
                 The condition choices for the field.
 
+                This may be a callable, which will cause the choices to load
+                only when needed.
+
+                Version Changed:
+                    5.3:
+                    Added support for a callable.
+
             mode_widget (django.forms.widgets.RadioSelect):
                 The widget for selecting the mode.
 
@@ -259,13 +290,51 @@ class ConditionsWidget(widgets.Widget):
         """
         super().__init__(attrs=attrs)
 
-        self.choices = choices
+        self._orig_choices = choices
+        self._choices = None
+
         self.mode_widget = mode_widget
         self.choice_widget = choice_widget
         self.operator_widget = operator_widget
         self.choice_kwargs = choice_kwargs or {}
         self.condition_errors = {}
 
+    @property
+    def choices(self) -> ConditionChoices:
+        """The condition choices for the widget.
+
+        Type:
+            djblets.conditions.choices.ConditionChoices
+        """
+        choices = self._choices
+
+        if choices is None:
+            orig_choices = self._orig_choices
+
+            if callable(orig_choices):
+                choices = orig_choices()
+            else:
+                choices = orig_choices
+
+            self._choices = choices
+
+        return choices
+
+    @choices.setter
+    def choices(
+        self,
+        choices: ConditionsWidgetChoices,
+    ) -> None:
+        """Set the condition choices for the widget.
+
+        Args:
+            value (djblets.conditions.choices.ConditionChoices or type or
+                   callable):
+                The new choices value.
+        """
+        self._orig_choices = choices
+        self._choices = None
+
     @property
     def media(self) -> widgets.Media:
         """Media needed for the widget.
@@ -804,6 +873,8 @@ class ConditionsWidget(widgets.Widget):
         obj.operator_widget = copy.deepcopy(self.operator_widget, memo)
         obj.condition_errors = copy.deepcopy(self.condition_errors, memo)
         obj.choice_kwargs = self.choice_kwargs.copy()
+        obj._orig_choices = self._orig_choices
+        obj._choices = None
 
         return obj
 
