Repository: ManimCommunity/manim
Pull request: Fix assertions and improve error messages when adding submobjects
Closes #3755.

Initially this PR only improved 2 things:
1. When adding into a `Mobject` a value which is not a `Mobject`, or there's an attempt to add a mobject to itself, **the error messages are now more descriptive**, indicating:
    - what's the value being added
    - what's its type
    - at which index is the issue happening
    - into which Mobject you're attempting to add the value

    Example:
```py
>>> img = ImageMobject("cat.jpg")
>>> img.add(Circle(), 3)
# TypeError: Only values of type Mobject can be added as submobjects of ImageMobject, but the value 3 (at index 1) is of type int.
>>> img.add(img)
# ValueError: Cannot add ImageMobject as a submobject of itself (at index 0).
```
2. **Refactor the code by creating a `Mobject._assert_valid_submobjects()` method which makes this check, and calling it on every method which required this assertion, which are `Mobject.add()`, `Mobject.insert()` and `Mobject.add_to_back()`.** Previously, those methods duplicated the code for checking the submobjects, but in different ways, with very different error messages, and occasionally not always checking the types of the submobjects.

But I noticed that, while `VGroup` overrided the `add()` method to be more restrictive (by checking that the submobjects are all instances of `VMobject`), the `VMobject` class itself didn't do that, which causes the issue #3755: you could add a `Mobject` to a `VMobject` such as `Circle`, and a `Mobject` could be part of the family of a `VGroup` without being a direct child of it.

Plus, the other methods `insert()` and `add_to_back()` weren't overridden, so one could still do this:
```py
>>> VGroup().add_to_back(Mobject())
VGroup(Mobject)
```

So, this PR also fixes that.

3. **Any `VMobject`, not only `VGroup`s, now asserts on `add()`, `insert()` and `add_to_back()` that all the submobjects are instances of `VMobject`.** To do that, it suffices to override the `_assert_valid_submobjects()` method in `VMobject`. It is not necessary to actually touch the `add()`, `insert()` and `add_to_back()` methods. _Because the `VGroup.add()` docstrings have an useful example, I decided to leave that override in the code, even if it now contains only a `return super().add(*vmobjects)` line._

```py
>>> Circle().add(Mobject())
# TypeError: Only values of type VMobject can be added as submobjects of Circle, but the value Mobject (at index 0) is of type Mobject. You can try adding this value into a Group instead.
```

Finally:

4. Repeat all the previous steps for `OpenGLMobject` and `OpenGLVMobject` to avoid breaking tests, and for more consistency.
5. Update the tests to take into account the more descriptive error messages.

## Reviewer Checklist
- [ ] The PR title is descriptive enough for the changelog, and the PR is labeled correctly
- [ ] If applicable: newly added non-private functions and classes have a docstring including a short summary and a PARAMETERS section
- [ ] If applicable: newly added functions and classes are tested


Problem statement:
`VGroup` cannot have a `Mobject` as a child, but `VMobject` can
## Description of bug / unexpected behavior
`Mobject` methods like `add()` assert that all the values passed are also instances of `Mobject`, before adding them as children (into `Mobject.submobjects`).

`VGroup` overrides `add()` to go even further, by asserting that the values are instances of the `VMobject` subclass, being more restrictive than the original assertion.

However, **the `VMobject` class itself does NOT have that assertion**, so you can actually add a `Mobject` to it which is not a `VMobject`.

Therefore, this is legal:
```py
Circle().add(Mobject())
```
but this raises a `TypeError`:
```py
VGroup().add(Mobject())
```

A direct consequence of this is that this is allowed:
```py
circle = Circle()
circle.add(Mobject())
group = VGroup()
group.add(circle)
```
creating a `VGroup` which contains a `Mobject` in its family, even though it's not a direct child / submobject.

## Expected behavior
The `VMobject` itself should assert that only `VMobject`s can be added to it. Adding a `Mobject` to a `VMobject` such as `Circle` should be illegal and also throw a `TypeError`. By doing this, `VGroup` would automatically do the assertion as well, as it's a subclass of `VMobject`.

Hints from issue discussion:

Commit message: Joined string in OpenGLMobject error message

Patch to review:
diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py
index 9127c2a1d6..de009022e6 100644
--- a/manim/mobject/mobject.py
+++ b/manim/mobject/mobject.py
@@ -115,6 +115,63 @@ def __init__(
         self.generate_points()
         self.init_colors()
 
+    def _assert_valid_submobjects(self, submobjects: Iterable[Mobject]) -> Self:
+        """Check that all submobjects are actually instances of
+        :class:`Mobject`, and that none of them is ``self`` (a
+        :class:`Mobject` cannot contain itself).
+
+        This is an auxiliary function called when adding Mobjects to the
+        :attr:`submobjects` list.
+
+        This function is intended to be overridden by subclasses such as
+        :class:`VMobject`, which should assert that only other VMobjects
+        may be added into it.
+
+        Parameters
+        ----------
+        submobjects
+            The list containing values to validate.
+
+        Returns
+        -------
+        :class:`Mobject`
+            The Mobject itself.
+
+        Raises
+        ------
+        TypeError
+            If any of the values in `submobjects` is not a :class:`Mobject`.
+        ValueError
+            If there was an attempt to add a :class:`Mobject` as its own
+            submobject.
+        """
+        return self._assert_valid_submobjects_internal(submobjects, Mobject)
+
+    def _assert_valid_submobjects_internal(
+        self, submobjects: list[Mobject], mob_class: type[Mobject]
+    ) -> Self:
+        for i, submob in enumerate(submobjects):
+            if not isinstance(submob, mob_class):
+                error_message = (
+                    f"Only values of type {mob_class.__name__} can be added "
+                    f"as submobjects of {type(self).__name__}, but the value "
+                    f"{submob} (at index {i}) is of type "
+                    f"{type(submob).__name__}."
+                )
+                # Intended for subclasses such as VMobject, which
+                # cannot have regular Mobjects as submobjects
+                if isinstance(submob, Mobject):
+                    error_message += (
+                        " You can try adding this value into a Group instead."
+                    )
+                raise TypeError(error_message)
+            if submob is self:
+                raise ValueError(
+                    f"Cannot add {type(self).__name__} as a submobject of "
+                    f"itself (at index {i})."
+                )
+        return self
+
     @classmethod
     def animation_override_for(
         cls,
@@ -413,12 +470,19 @@ def add(self, *mobjects: Mobject) -> Self:
             >>> len(outer.submobjects)
             1
 
+        Only Mobjects can be added::
+
+            >>> outer.add(3)
+            Traceback (most recent call last):
+            ...
+            TypeError: Only values of type Mobject can be added as submobjects of Mobject, but the value 3 (at index 0) is of type int.
+
         Adding an object to itself raises an error::
 
             >>> outer.add(outer)
             Traceback (most recent call last):
             ...
-            ValueError: Mobject cannot contain self
+            ValueError: Cannot add Mobject as a submobject of itself (at index 0).
 
         A given mobject cannot be added as a submobject
         twice to some parent::
@@ -432,12 +496,7 @@ def add(self, *mobjects: Mobject) -> Self:
             [child]
 
         """
-        for m in mobjects:
-            if not isinstance(m, Mobject):
-                raise TypeError("All submobjects must be of type Mobject")
-            if m is self:
-                raise ValueError("Mobject cannot contain self")
-
+        self._assert_valid_submobjects(mobjects)
         unique_mobjects = remove_list_redundancies(mobjects)
         if len(mobjects) != len(unique_mobjects):
             logger.warning(
@@ -463,10 +522,7 @@ def insert(self, index: int, mobject: Mobject) -> None:
         mobject
             The mobject to be inserted.
         """
-        if not isinstance(mobject, Mobject):
-            raise TypeError("All submobjects must be of type Mobject")
-        if mobject is self:
-            raise ValueError("Mobject cannot contain self")
+        self._assert_valid_submobjects([mobject])
         self.submobjects.insert(index, mobject)
 
     def __add__(self, mobject: Mobject):
@@ -519,13 +575,7 @@ def add_to_back(self, *mobjects: Mobject) -> Self:
         :meth:`add`
 
         """
-        if self in mobjects:
-            raise ValueError("A mobject shouldn't contain itself")
-
-        for mobject in mobjects:
-            if not isinstance(mobject, Mobject):
-                raise TypeError("All submobjects must be of type Mobject")
-
+        self._assert_valid_submobjects(mobjects)
         self.remove(*mobjects)
         # dict.fromkeys() removes duplicates while maintaining order
         self.submobjects = list(dict.fromkeys(mobjects)) + self.submobjects
diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py
index d64f9c0211..df7422b573 100644
--- a/manim/mobject/opengl/opengl_mobject.py
+++ b/manim/mobject/opengl/opengl_mobject.py
@@ -165,6 +165,64 @@ def __init__(
 
         self.should_render = should_render
 
+    def _assert_valid_submobjects(self, submobjects: Iterable[OpenGLMobject]) -> Self:
+        """Check that all submobjects are actually instances of
+        :class:`OpenGLMobject`, and that none of them is
+        ``self`` (an :class:`OpenGLMobject` cannot contain itself).
+
+        This is an auxiliary function called when adding OpenGLMobjects to the
+        :attr:`submobjects` list.
+
+        This function is intended to be overridden by subclasses such as
+        :class:`OpenGLVMobject`, which should assert that only other
+        OpenGLVMobjects may be added into it.
+
+        Parameters
+        ----------
+        submobjects
+            The list containing values to validate.
+
+        Returns
+        -------
+        :class:`OpenGLMobject`
+            The OpenGLMobject itself.
+
+        Raises
+        ------
+        TypeError
+            If any of the values in `submobjects` is not an
+            :class:`OpenGLMobject`.
+        ValueError
+            If there was an attempt to add an :class:`OpenGLMobject` as its own
+            submobject.
+        """
+        return self._assert_valid_submobjects_internal(submobjects, OpenGLMobject)
+
+    def _assert_valid_submobjects_internal(
+        self, submobjects: list[OpenGLMobject], mob_class: type[OpenGLMobject]
+    ) -> Self:
+        for i, submob in enumerate(submobjects):
+            if not isinstance(submob, mob_class):
+                error_message = (
+                    f"Only values of type {mob_class.__name__} can be added "
+                    f"as submobjects of {type(self).__name__}, but the value "
+                    f"{submob} (at index {i}) is of type "
+                    f"{type(submob).__name__}."
+                )
+                # Intended for subclasses such as OpenGLVMobject, which
+                # cannot have regular OpenGLMobjects as submobjects
+                if isinstance(submob, OpenGLMobject):
+                    error_message += (
+                        " You can try adding this value into a Group instead."
+                    )
+                raise TypeError(error_message)
+            if submob is self:
+                raise ValueError(
+                    f"Cannot add {type(self).__name__} as a submobject of "
+                    f"itself (at index {i})."
+                )
+        return self
+
     @classmethod
     def __init_subclass__(cls, **kwargs):
         super().__init_subclass__(**kwargs)
@@ -734,28 +792,33 @@ def add(
             >>> len(outer.submobjects)
             1
 
+        Only OpenGLMobjects can be added::
+
+            >>> outer.add(3)
+            Traceback (most recent call last):
+            ...
+            TypeError: Only values of type OpenGLMobject can be added as submobjects of OpenGLMobject, but the value 3 (at index 0) is of type int.
+
         Adding an object to itself raises an error::
 
             >>> outer.add(outer)
             Traceback (most recent call last):
             ...
-            ValueError: OpenGLMobject cannot contain self
+            ValueError: Cannot add OpenGLMobject as a submobject of itself (at index 0).
 
         """
         if update_parent:
             assert len(mobjects) == 1, "Can't set multiple parents."
             mobjects[0].parent = self
 
-        if self in mobjects:
-            raise ValueError("OpenGLMobject cannot contain self")
+        self._assert_valid_submobjects(mobjects)
+
         if any(mobjects.count(elem) > 1 for elem in mobjects):
             logger.warning(
                 "Attempted adding some Mobject as a child more than once, "
                 "this is not possible. Repetitions are ignored.",
             )
         for mobject in mobjects:
-            if not isinstance(mobject, OpenGLMobject):
-                raise TypeError("All submobjects must be of type OpenGLMobject")
             if mobject not in self.submobjects:
                 self.submobjects.append(mobject)
             if self not in mobject.parents:
@@ -784,11 +847,7 @@ def insert(self, index: int, mobject: OpenGLMobject, update_parent: bool = False
         if update_parent:
             mobject.parent = self
 
-        if mobject is self:
-            raise ValueError("OpenGLMobject cannot contain self")
-
-        if not isinstance(mobject, OpenGLMobject):
-            raise TypeError("All submobjects must be of type OpenGLMobject")
+        self._assert_valid_submobjects([mobject])
 
         if mobject not in self.submobjects:
             self.submobjects.insert(index, mobject)
@@ -880,10 +939,12 @@ def add_to_back(self, *mobjects: OpenGLMobject) -> OpenGLMobject:
         :meth:`add`
 
         """
+        self._assert_valid_submobjects(mobjects)
         self.submobjects = list_update(mobjects, self.submobjects)
         return self
 
     def replace_submobject(self, index, new_submob):
+        self._assert_valid_submobjects([new_submob])
         old_submob = self.submobjects[index]
         if self in old_submob.parents:
             old_submob.parents.remove(self)
diff --git a/manim/mobject/opengl/opengl_vectorized_mobject.py b/manim/mobject/opengl/opengl_vectorized_mobject.py
index 30d47b8882..ac1dad7c46 100644
--- a/manim/mobject/opengl/opengl_vectorized_mobject.py
+++ b/manim/mobject/opengl/opengl_vectorized_mobject.py
@@ -160,6 +160,9 @@ def __init__(
         if stroke_color is not None:
             self.stroke_color = ManimColor.parse(stroke_color)
 
+    def _assert_valid_submobjects(self, submobjects: Iterable[OpenGLVMobject]) -> Self:
+        return self._assert_valid_submobjects_internal(submobjects, OpenGLVMobject)
+
     def get_group_class(self):
         return OpenGLVGroup
 
@@ -1752,8 +1755,6 @@ def construct(self):
                         (gr-circle_red).animate.shift(RIGHT)
                     )
         """
-        if not all(isinstance(m, OpenGLVMobject) for m in vmobjects):
-            raise TypeError("All submobjects must be of type OpenGLVMobject")
         return super().add(*vmobjects)
 
     def __add__(self, vmobject):
diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py
index 6a2fffd2cb..8ecb4b6db6 100644
--- a/manim/mobject/types/vectorized_mobject.py
+++ b/manim/mobject/types/vectorized_mobject.py
@@ -179,6 +179,9 @@ def __init__(
         if stroke_color is not None:
             self.stroke_color = ManimColor.parse(stroke_color)
 
+    def _assert_valid_submobjects(self, submobjects: Iterable[VMobject]) -> Self:
+        return self._assert_valid_submobjects_internal(submobjects, VMobject)
+
     # OpenGL compatibility
     @property
     def n_points_per_curve(self) -> int:
@@ -2052,14 +2055,6 @@ def construct(self):
                         (gr-circle_red).animate.shift(RIGHT)
                     )
         """
-        for m in vmobjects:
-            if not isinstance(m, (VMobject, OpenGLVMobject)):
-                raise TypeError(
-                    f"All submobjects of {self.__class__.__name__} must be of type VMobject. "
-                    f"Got {repr(m)} ({type(m).__name__}) instead. "
-                    "You can try using `Group` instead."
-                )
-
         return super().add(*vmobjects)
 
     def __add__(self, vmobject: VMobject) -> Self:
diff --git a/tests/module/mobject/mobject/test_mobject.py b/tests/module/mobject/mobject/test_mobject.py
index bb228eb798..7d7aaf51f0 100644
--- a/tests/module/mobject/mobject/test_mobject.py
+++ b/tests/module/mobject/mobject/test_mobject.py
@@ -29,12 +29,20 @@ def test_mobject_add():
     obj = Mobject()
 
     # a Mobject cannot contain itself
-    with pytest.raises(ValueError):
-        obj.add(obj)
+    with pytest.raises(ValueError) as add_self_info:
+        obj.add(Mobject(), obj, Mobject())
+    print(str(add_self_info.value))
+    assert str(add_self_info.value) == (
+        "Cannot add Mobject as a submobject of itself (at index 1)."
+    )
 
     # can only add Mobjects
-    with pytest.raises(TypeError):
-        obj.add("foo")
+    with pytest.raises(TypeError) as add_str_info:
+        obj.add(Mobject(), Mobject(), "foo")
+    assert str(add_str_info.value) == (
+        "Only values of type Mobject can be added as submobjects of Mobject, "
+        "but the value foo (at index 2) is of type str."
+    )
 
 
 def test_mobject_remove():
diff --git a/tests/module/mobject/types/vectorized_mobject/test_vectorized_mobject.py b/tests/module/mobject/types/vectorized_mobject/test_vectorized_mobject.py
index a17383fd98..f682370462 100644
--- a/tests/module/mobject/types/vectorized_mobject/test_vectorized_mobject.py
+++ b/tests/module/mobject/types/vectorized_mobject/test_vectorized_mobject.py
@@ -18,6 +18,53 @@
 from manim.constants import PI
 
 
+def test_vmobject_add():
+    """Test the VMobject add method."""
+    obj = VMobject()
+    assert len(obj.submobjects) == 0
+
+    obj.add(VMobject())
+    assert len(obj.submobjects) == 1
+
+    # Can't add non-VMobject values to a VMobject.
+    with pytest.raises(TypeError) as add_int_info:
+        obj.add(3)
+    assert str(add_int_info.value) == (
+        "Only values of type VMobject can be added as submobjects of VMobject, "
+        "but the value 3 (at index 0) is of type int."
+    )
+    assert len(obj.submobjects) == 1
+
+    # Plain Mobjects can't be added to a VMobject if they're not
+    # VMobjects. Suggest adding them into a Group instead.
+    with pytest.raises(TypeError) as add_mob_info:
+        obj.add(Mobject())
+    assert str(add_mob_info.value) == (
+        "Only values of type VMobject can be added as submobjects of VMobject, "
+        "but the value Mobject (at index 0) is of type Mobject. You can try "
+        "adding this value into a Group instead."
+    )
+    assert len(obj.submobjects) == 1
+
+    with pytest.raises(TypeError) as add_vmob_and_mob_info:
+        # If only one of the added objects is not an instance of VMobject, none of them should be added
+        obj.add(VMobject(), Mobject())
+    assert str(add_vmob_and_mob_info.value) == (
+        "Only values of type VMobject can be added as submobjects of VMobject, "
+        "but the value Mobject (at index 1) is of type Mobject. You can try "
+        "adding this value into a Group instead."
+    )
+    assert len(obj.submobjects) == 1
+
+    # A VMobject or VGroup cannot contain itself.
+    with pytest.raises(ValueError) as add_self_info:
+        obj.add(obj)
+    assert str(add_self_info.value) == (
+        "Cannot add VMobject as a submobject of itself (at index 0)."
+    )
+    assert len(obj.submobjects) == 1
+
+
 def test_vmobject_point_from_propotion():
     obj = VMobject()
 
@@ -79,28 +126,77 @@ def test_vgroup_init():
     VGroup()
     VGroup(VMobject())
     VGroup(VMobject(), VMobject())
-    with pytest.raises(TypeError):
+
+    # A VGroup cannot contain non-VMobject values.
+    with pytest.raises(TypeError) as init_with_float_info:
+        VGroup(3.0)
+    assert str(init_with_float_info.value) == (
+        "Only values of type VMobject can be added as submobjects of VGroup, "
+        "but the value 3.0 (at index 0) is of type float."
+    )
+
+    with pytest.raises(TypeError) as init_with_mob_info:
         VGroup(Mobject())
-    with pytest.raises(TypeError):
-        VGroup(Mobject(), Mobject())
+    assert str(init_with_mob_info.value) == (
+        "Only values of type VMobject can be added as submobjects of VGroup, "
+        "but the value Mobject (at index 0) is of type Mobject. You can try "
+        "adding this value into a Group instead."
+    )
+
+    with pytest.raises(TypeError) as init_with_vmob_and_mob_info:
+        VGroup(VMobject(), Mobject())
+    assert str(init_with_vmob_and_mob_info.value) == (
+        "Only values of type VMobject can be added as submobjects of VGroup, "
+        "but the value Mobject (at index 1) is of type Mobject. You can try "
+        "adding this value into a Group instead."
+    )
 
 
 def test_vgroup_add():
     """Test the VGroup add method."""
     obj = VGroup()
     assert len(obj.submobjects) == 0
+
     obj.add(VMobject())
     assert len(obj.submobjects) == 1
-    with pytest.raises(TypeError):
+
+    # Can't add non-VMobject values to a VMobject or VGroup.
+    with pytest.raises(TypeError) as add_int_info:
+        obj.add(3)
+    assert str(add_int_info.value) == (
+        "Only values of type VMobject can be added as submobjects of VGroup, "
+        "but the value 3 (at index 0) is of type int."
+    )
+    assert len(obj.submobjects) == 1
+
+    # Plain Mobjects can't be added to a VMobject or VGroup if they're not
+    # VMobjects. Suggest adding them into a Group instead.
+    with pytest.raises(TypeError) as add_mob_info:
         obj.add(Mobject())
+    assert str(add_mob_info.value) == (
+        "Only values of type VMobject can be added as submobjects of VGroup, "
+        "but the value Mobject (at index 0) is of type Mobject. You can try "
+        "adding this value into a Group instead."
+    )
     assert len(obj.submobjects) == 1
-    with pytest.raises(TypeError):
-        # If only one of the added object is not an instance of VMobject, none of them should be added
+
+    with pytest.raises(TypeError) as add_vmob_and_mob_info:
+        # If only one of the added objects is not an instance of VMobject, none of them should be added
         obj.add(VMobject(), Mobject())
+    assert str(add_vmob_and_mob_info.value) == (
+        "Only values of type VMobject can be added as submobjects of VGroup, "
+        "but the value Mobject (at index 1) is of type Mobject. You can try "
+        "adding this value into a Group instead."
+    )
     assert len(obj.submobjects) == 1
-    with pytest.raises(ValueError):
-        # a Mobject cannot contain itself
+
+    # A VMobject or VGroup cannot contain itself.
+    with pytest.raises(ValueError) as add_self_info:
         obj.add(obj)
+    assert str(add_self_info.value) == (
+        "Cannot add VGroup as a submobject of itself (at index 0)."
+    )
+    assert len(obj.submobjects) == 1
 
 
 def test_vgroup_add_dunder():
diff --git a/tests/opengl/test_opengl_mobject.py b/tests/opengl/test_opengl_mobject.py
index f1a58fa6a3..319f6c7442 100644
--- a/tests/opengl/test_opengl_mobject.py
+++ b/tests/opengl/test_opengl_mobject.py
@@ -27,13 +27,21 @@ def test_opengl_mobject_add(using_opengl_renderer):
     assert obj.add(OpenGLMobject()) is obj
     obj = OpenGLMobject()
 
-    # a OpenGLMobject cannot contain itself
-    with pytest.raises(ValueError):
-        obj.add(obj)
-
-    # can only add OpenGLMobjects
-    with pytest.raises(TypeError):
-        obj.add("foo")
+    # an OpenGLMobject cannot contain itself
+    with pytest.raises(ValueError) as add_self_info:
+        obj.add(OpenGLMobject(), obj, OpenGLMobject())
+    print(str(add_self_info.value))
+    assert str(add_self_info.value) == (
+        "Cannot add OpenGLMobject as a submobject of itself (at index 1)."
+    )
+
+    # can only add Mobjects
+    with pytest.raises(TypeError) as add_str_info:
+        obj.add(OpenGLMobject(), OpenGLMobject(), "foo")
+    assert str(add_str_info.value) == (
+        "Only values of type OpenGLMobject can be added as submobjects of "
+        "OpenGLMobject, but the value foo (at index 2) is of type str."
+    )
 
 
 def test_opengl_mobject_remove(using_opengl_renderer):
diff --git a/tests/opengl/test_opengl_vectorized_mobject.py b/tests/opengl/test_opengl_vectorized_mobject.py
index 8e0ba22582..d0006ea190 100644
--- a/tests/opengl/test_opengl_vectorized_mobject.py
+++ b/tests/opengl/test_opengl_vectorized_mobject.py
@@ -8,6 +8,53 @@
 from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
 
 
+def test_opengl_vmobject_add(using_opengl_renderer):
+    """Test the OpenGLVMobject add method."""
+    obj = OpenGLVMobject()
+    assert len(obj.submobjects) == 0
+
+    obj.add(OpenGLVMobject())
+    assert len(obj.submobjects) == 1
+
+    # Can't add non-OpenGLVMobject values to a VMobject.
+    with pytest.raises(TypeError) as add_int_info:
+        obj.add(3)
+    assert str(add_int_info.value) == (
+        "Only values of type OpenGLVMobject can be added as submobjects of "
+        "OpenGLVMobject, but the value 3 (at index 0) is of type int."
+    )
+    assert len(obj.submobjects) == 1
+
+    # Plain OpenGLMobjects can't be added to a OpenGLVMobject if they're not
+    # OpenGLVMobjects. Suggest adding them into an OpenGLGroup instead.
+    with pytest.raises(TypeError) as add_mob_info:
+        obj.add(OpenGLMobject())
+    assert str(add_mob_info.value) == (
+        "Only values of type OpenGLVMobject can be added as submobjects of "
+        "OpenGLVMobject, but the value OpenGLMobject (at index 0) is of type "
+        "OpenGLMobject. You can try adding this value into a Group instead."
+    )
+    assert len(obj.submobjects) == 1
+
+    with pytest.raises(TypeError) as add_vmob_and_mob_info:
+        # If only one of the added objects is not an instance of VMobject, none of them should be added
+        obj.add(OpenGLVMobject(), OpenGLMobject())
+    assert str(add_vmob_and_mob_info.value) == (
+        "Only values of type OpenGLVMobject can be added as submobjects of "
+        "OpenGLVMobject, but the value OpenGLMobject (at index 1) is of type "
+        "OpenGLMobject. You can try adding this value into a Group instead."
+    )
+    assert len(obj.submobjects) == 1
+
+    # A VMobject or VGroup cannot contain itself.
+    with pytest.raises(ValueError) as add_self_info:
+        obj.add(obj)
+    assert str(add_self_info.value) == (
+        "Cannot add OpenGLVMobject as a submobject of itself (at index 0)."
+    )
+    assert len(obj.submobjects) == 1
+
+
 def test_opengl_vmobject_point_from_propotion(using_opengl_renderer):
     obj = OpenGLVMobject()
 
@@ -37,28 +84,77 @@ def test_vgroup_init(using_opengl_renderer):
     VGroup()
     VGroup(OpenGLVMobject())
     VGroup(OpenGLVMobject(), OpenGLVMobject())
-    with pytest.raises(TypeError):
+
+    # A VGroup cannot contain non-VMobject values.
+    with pytest.raises(TypeError) as init_with_float_info:
+        VGroup(3.0)
+    assert str(init_with_float_info.value) == (
+        "Only values of type OpenGLVMobject can be added as submobjects of "
+        "VGroup, but the value 3.0 (at index 0) is of type float."
+    )
+
+    with pytest.raises(TypeError) as init_with_mob_info:
         VGroup(OpenGLMobject())
-    with pytest.raises(TypeError):
-        VGroup(OpenGLMobject(), OpenGLMobject())
+    assert str(init_with_mob_info.value) == (
+        "Only values of type OpenGLVMobject can be added as submobjects of "
+        "VGroup, but the value OpenGLMobject (at index 0) is of type "
+        "OpenGLMobject. You can try adding this value into a Group instead."
+    )
+
+    with pytest.raises(TypeError) as init_with_vmob_and_mob_info:
+        VGroup(OpenGLVMobject(), OpenGLMobject())
+    assert str(init_with_vmob_and_mob_info.value) == (
+        "Only values of type OpenGLVMobject can be added as submobjects of "
+        "VGroup, but the value OpenGLMobject (at index 1) is of type "
+        "OpenGLMobject. You can try adding this value into a Group instead."
+    )
 
 
 def test_vgroup_add(using_opengl_renderer):
     """Test the VGroup add method."""
     obj = VGroup()
     assert len(obj.submobjects) == 0
+
     obj.add(OpenGLVMobject())
     assert len(obj.submobjects) == 1
-    with pytest.raises(TypeError):
+
+    # Can't add non-OpenGLVMobject values to a VMobject.
+    with pytest.raises(TypeError) as add_int_info:
+        obj.add(3)
+    assert str(add_int_info.value) == (
+        "Only values of type OpenGLVMobject can be added as submobjects of "
+        "VGroup, but the value 3 (at index 0) is of type int."
+    )
+    assert len(obj.submobjects) == 1
+
+    # Plain OpenGLMobjects can't be added to a OpenGLVMobject if they're not
+    # OpenGLVMobjects. Suggest adding them into an OpenGLGroup instead.
+    with pytest.raises(TypeError) as add_mob_info:
         obj.add(OpenGLMobject())
+    assert str(add_mob_info.value) == (
+        "Only values of type OpenGLVMobject can be added as submobjects of "
+        "VGroup, but the value OpenGLMobject (at index 0) is of type "
+        "OpenGLMobject. You can try adding this value into a Group instead."
+    )
     assert len(obj.submobjects) == 1
-    with pytest.raises(TypeError):
-        # If only one of the added object is not an instance of OpenGLVMobject, none of them should be added
+
+    with pytest.raises(TypeError) as add_vmob_and_mob_info:
+        # If only one of the added objects is not an instance of VMobject, none of them should be added
         obj.add(OpenGLVMobject(), OpenGLMobject())
+    assert str(add_vmob_and_mob_info.value) == (
+        "Only values of type OpenGLVMobject can be added as submobjects of "
+        "VGroup, but the value OpenGLMobject (at index 1) is of type "
+        "OpenGLMobject. You can try adding this value into a Group instead."
+    )
     assert len(obj.submobjects) == 1
-    with pytest.raises(ValueError):
-        # a OpenGLMobject cannot contain itself
+
+    # A VMobject or VGroup cannot contain itself.
+    with pytest.raises(ValueError) as add_self_info:
         obj.add(obj)
+    assert str(add_self_info.value) == (
+        "Cannot add VGroup as a submobject of itself (at index 0)."
+    )
+    assert len(obj.submobjects) == 1
 
 
 def test_vgroup_add_dunder(using_opengl_renderer):


Code review comments:
diff_hunk: @@ -29,12 +29,20 @@ def test_mobject_add():
     obj = Mobject()
 
     # a Mobject cannot contain itself
-    with pytest.raises(ValueError):
-        obj.add(obj)
+    with pytest.raises(ValueError) as add_self_info:
+        obj.add(Mobject(), obj, Mobject())
+    print(str(add_self_info.value))
path: tests/module/mobject/mobject/test_mobject.py
text: @user1:
```suggestion
```
Is there a need to print it?

@author:
Whoops, that shouldn't have been there. I just removed it.