Repository: PrefectHQ/prefect

Issue:
User Warning when child Task class uses a reserved fieldname
Occasionally when subclassing `prefect.Task`  I will accidentally use field names that get overwritten by the `prefect.Task.__init__` parent class.  For instance, while writing a data loading task, I attempted to use a `target`  field to specify the destination of my data.  This of course was overridden by the `prefect.Task`'s `__init__` method, but took a little while to figure out and remember that `target` is actually a reserved field name for a prefect.Task.  I thought it might be helpful to throw a user warning in this case when the prefect.Task parent class overrides a field used by the child task class.

## Example
```
task = UserTask(target='my_target')
```

A warning is thrown when `super().__init__()` is called at end of `UserTask.__init__()`:
```
UserWarning: Field `target` is a reserved for use by UserTask's parent class, `prefect.Task`.  User should change this fieldname to avoid unintended interactions with `prefect.Task.__init__` method.
```


Discussion:

Patch:
diff --git a/changes/pr4011.yaml b/changes/pr4011.yaml
new file mode 100644
index 0000000000..9fba7663d0
--- /dev/null
+++ b/changes/pr4011.yaml
@@ -0,0 +1,2 @@
+enhancement:
+  - "Add warning if user-defined class shadows an attribute used by the base class - [#4011](https://github.com/PrefectHQ/prefect/pull/4011)"
diff --git a/src/prefect/core/task.py b/src/prefect/core/task.py
index c5880b8d9b..30c7008a28 100644
--- a/src/prefect/core/task.py
+++ b/src/prefect/core/task.py
@@ -17,6 +17,7 @@ from typing import (
     Mapping,
     Optional,
     Union,
+    Tuple,
 )
 
 import prefect
@@ -163,6 +164,17 @@ class TaskMetaclass(type):
         # also get passed through __new__
         return type.__new__(cls, name, parents, methods)  # type: ignore
 
+    @property
+    def _reserved_attributes(self) -> Tuple[str]:
+        """A tuple of attributes reserved for use by the `Task` class.
+
+        Dynamically computed to make it easier to keep up to date. Lazily
+        computed to avoid circular import issues.
+        """
+        if not hasattr(Task, "_cached_reserved_attributes"):
+            Task._cached_reserved_attributes = tuple(sorted(Task().__dict__))  # type: ignore
+        return Task._cached_reserved_attributes  # type: ignore
+
 
 class instance_property:
     """Like property, but only available on instances, not the class"""
@@ -313,6 +325,15 @@ class Task(metaclass=TaskMetaclass):
         task_run_name: Union[str, Callable] = None,
         nout: int = None,
     ):
+        if type(self) is not Task:
+            for attr in Task._reserved_attributes:
+                if hasattr(self, attr):
+                    warnings.warn(
+                        f"`{type(self).__name__}` sets a `{attr}` attribute, which "
+                        "will be overwritten by `prefect.Task`. Please rename this "
+                        "attribute to avoid this issue."
+                    )
+
         self.name = name or type(self).__name__
         self.slug = slug
 
@@ -1242,6 +1263,7 @@ EXTRA_CALL_PARAMETERS = [
     if p.kind == inspect.Parameter.KEYWORD_ONLY
 ]
 
+
 # DEPRECATED - this is to allow backwards-compatible access to Parameters
 # https://github.com/PrefectHQ/prefect/pull/2758
 from .parameter import Parameter as _Parameter


Test patch:
diff --git a/tests/core/test_task.py b/tests/core/test_task.py
index 280227db39..40b458a77f 100644
--- a/tests/core/test_task.py
+++ b/tests/core/test_task.py
@@ -743,7 +743,7 @@ def test_passing_task_to_task_constructor_raises_helpful_warning():
             self.b = b
             super().__init__(**kwargs)
 
-    with Flow("test") as flow:
+    with Flow("test"):
         a = Task()()
         with pytest.warns(
             UserWarning, match="A Task was passed as an argument to MyTask"
@@ -752,3 +752,15 @@ def test_passing_task_to_task_constructor_raises_helpful_warning():
         # Warning doesn't stop normal operation
         assert t.a == 1
         assert t.b == a
+
+
+def test_task_init_uses_reserved_attribute_raises_helpful_warning():
+    class MyTask(Task):
+        def __init__(self, **kwargs):
+            self.a = 1
+            self.target = "oh no!"
+            super().__init__(**kwargs)
+
+    with Flow("test"):
+        with pytest.warns(UserWarning, match="`MyTask` sets a `target` attribute"):
+            MyTask()