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:
This is a great idea - is this something you're interested in contributing?
Yes I'm happy contribute this, should be straightforward. I can get a PR together. At the top of `prefect.Task.__init__`, I will likely just check `inspect.signature(self.__init__)` against fields that already exist in `self` and throw a UserWarning if there's an collision.  Sound reasonable?
> Yes I'm happy contribute this, should be straightforward. I can get a PR together. At the top of `prefect.Task.__init__`, I will likely just check `inspect.signature(self.__init__)` against fields that already exist in `self` and throw a UserWarning if there's an collision. Sound reasonable?

I believe you'll need to make sure `type(self) != Task` so you don't throw warnings for the base class but otherwise that makes sense to me. I'd probably put it in a private method `_check_for_name_collisions` or something.
This kind of check belongs in the metaclass (so it's done at class definition time, not instance creation time). You'd want to handle it within the `__new__` call defined here: https://github.com/PrefectHQ/prefect/blob/38ec61fe7c4a5cd5714a8bddfb3fd021e1f6bd7d/src/prefect/core/task.py#L122 (agree with michael that breaking this check out into a helper method would be nice).

Resolving patch:
diff --git a/src/prefect/core/task.py b/src/prefect/core/task.py
--- a/src/prefect/core/task.py
+++ b/src/prefect/core/task.py
@@ -17,6 +17,7 @@
     Mapping,
     Optional,
     Union,
+    Tuple,
 )
 
 import prefect
@@ -163,6 +164,17 @@ def init(self: Any, *args: Any, **kwargs: Any) -> None:
         # 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 @@ def __init__(
         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 @@ def __le__(self, other: object) -> "Task":
     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
--- a/tests/core/test_task.py
+++ b/tests/core/test_task.py
@@ -743,7 +743,7 @@ def __init__(self, a, b, **kwargs):
             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 __init__(self, a, b, **kwargs):
         # 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()