diff --git a/fiftyone/core/odm/__init__.py b/fiftyone/core/odm/__init__.py
index d8331139b9..e4424060ad 100644
--- a/fiftyone/core/odm/__init__.py
+++ b/fiftyone/core/odm/__init__.py
@@ -63,6 +63,7 @@
 from .embedded_document import (
     BaseEmbeddedDocument,
     DynamicEmbeddedDocument,
+    DynamicEmbeddedDocumentException,
     EmbeddedDocument,
 )
 from .frame import (
diff --git a/fiftyone/core/odm/embedded_document.py b/fiftyone/core/odm/embedded_document.py
index f86df11895..7a78fa7ba7 100644
--- a/fiftyone/core/odm/embedded_document.py
+++ b/fiftyone/core/odm/embedded_document.py
@@ -5,6 +5,7 @@
 | `voxel51.com <https://voxel51.com/>`_
 |
 """
+import re
 import mongoengine
 
 from .document import DynamicMixin, MongoEngineBaseDocument
@@ -30,6 +31,12 @@ def __init__(self, *args, **kwargs):
         self.validate()
 
 
+class DynamicEmbeddedDocumentException(Exception):
+    """Exception raised when an error occurs in a dynamic document operation."""
+
+    pass
+
+
 class DynamicEmbeddedDocument(
     DynamicMixin,
     BaseEmbeddedDocument,
@@ -45,12 +52,50 @@ class DynamicEmbeddedDocument(
     meta = {"abstract": True, "allow_inheritance": True}
 
     def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
+        try:
+            super().__init__(*args, **kwargs)
+        except AttributeError as e:
+            self._raise_reserved_attribute_exception(e)
+            raise e
+
         self.validate()
 
+    def __setattr__(self, name, value):
+        try:
+            super().__setattr__(name, value)
+        except AttributeError as e:
+            self._raise_reserved_attribute_exception(e)
+            raise e
+
     def __hash__(self):
         return hash(str(self))
 
+    def _raise_reserved_attribute_exception(self, e):
+        key = self._extract_attribute_from_exception(e)
+        if key is not None:
+            if isinstance(getattr(self.__class__, key, None), property):
+                raise DynamicEmbeddedDocumentException(
+                    f"Invalid attribute name '{key}'. '{key}' is a reserved keyword for {self.__class__.__name__} objects"
+                )
+
+        if "can't set attribute" in str(e):
+            raise DynamicEmbeddedDocumentException(
+                f"One or more attributes are reserved keywords for `{self.__class__.__name__}` objects"
+            )
+
+    @staticmethod
+    def _extract_attribute_from_exception(e):
+        pattern = (
+            r"(?:property '(?P<attribute1>\w+)' of '[^']+' object has no setter|"
+            r"can't set attribute '(?P<attribute2>\w+)')"
+        )
+        match = re.match(pattern, str(e))
+
+        if match:
+            return match.group("attribute1") or match.group("attribute2")
+
+        return None
+
     def _get_field(self, field_name, allow_missing=False):
         # pylint: disable=no-member
         chunks = field_name.split(".", 1)
diff --git a/tests/unittests/documents.py b/tests/unittests/documents.py
index d33c852423..4eb059d8d3 100644
--- a/tests/unittests/documents.py
+++ b/tests/unittests/documents.py
@@ -9,12 +9,17 @@
 | `voxel51.com <https://voxel51.com/>`_
 |
 """
-
 import unittest
+
 from mongoengine import EmbeddedDocument
 from mongoengine.errors import ValidationError
+
+import fiftyone as fo
 from fiftyone.core.fields import EmbeddedDocumentListField
-from fiftyone.core.odm.dataset import SampleFieldDocument
+from fiftyone.core.odm import (
+    SampleFieldDocument,
+    DynamicEmbeddedDocumentException,
+)
 
 
 class MockEmbeddedDocument(EmbeddedDocument):
@@ -46,3 +51,64 @@ def test_validate(self):
         invalid_input = valid_input + [MockEmbeddedDocument()]
         with self.assertRaises(ValidationError):
             list_field.validate(invalid_input)
+
+
+class TestDetection(unittest.TestCase):
+    def test_valid_detection_creation(self):
+        detection = fo.Detection(
+            label="car",
+            bounding_box=[0.1, 0.1, 0.2, 0.2],
+            confidence=0.95,
+            custom_attr="test",
+            another_custom_attr=123,
+        )
+
+        self.assertEqual(detection.label, "car")
+        self.assertEqual(detection.bounding_box, [0.1, 0.1, 0.2, 0.2])
+        self.assertEqual(detection.confidence, 0.95)
+
+        # pylint: disable=no-member
+        self.assertEqual(detection.custom_attr, "test")
+        self.assertEqual(detection.another_custom_attr, 123)
+
+        detection.custom_attr = "value"
+        self.assertEqual(detection.custom_attr, "value")
+
+    def test_reserved_attribute_raises_exception(self):
+        # `has_mask` is a property of `Detection`
+
+        with self.assertRaises(DynamicEmbeddedDocumentException):
+            fo.Detection(has_mask="not allowed")
+
+        with self.assertRaises(DynamicEmbeddedDocumentException):
+            fo.Detection(
+                label="car",
+                bounding_box=[0.1, 0.1, 0.2, 0.2],
+                confidence=0.95,
+                has_mask="not allowed",
+                custom_attr="foo",
+                another_custom_attr=123,
+            )
+
+        detection = fo.Detection()
+
+        with self.assertRaises(DynamicEmbeddedDocumentException):
+            detection.has_mask = "not allowed"
+
+    def test_extract_attribute_from_exception(self):
+        # Test property setter pattern
+        exception1 = AttributeError(
+            "property 'name' of 'MyDoc' object has no setter"
+        )
+        attr1 = fo.Detection._extract_attribute_from_exception(exception1)
+        self.assertEqual(attr1, "name")
+
+        # Test can't set attribute pattern
+        exception2 = AttributeError("can't set attribute 'age'")
+        attr2 = fo.Detection._extract_attribute_from_exception(exception2)
+        self.assertEqual(attr2, "age")
+
+        # Test no match
+        exception3 = AttributeError("some other error")
+        attr3 = fo.Detection._extract_attribute_from_exception(exception3)
+        self.assertIsNone(attr3)