def __init__(
        self,
        value: Union[str, bytes] = "",
        image_path: str = "",
        predecessors: List['Variable']=None,
        requires_grad: bool=True,
        *,
        role_description: str):
        """The main thing. Nodes in the computation graph. Really the heart and soul of textgrad.

        :param value: The string value of this variable, defaults to "". In the future, we'll go multimodal, for sure!
        :type value: str or bytes, optional
        :param image_path: The path to the image file, defaults to "". If present we will read from disk or download the image.
        :type image_path: str, optional
        :param predecessors: predecessors of this variable in the computation graph, defaults to None. Here, for instance, if we have a prompt -> response through an LLM call, we'd call the prompt the predecessor, and the response the successor. 
        :type predecessors: List[Variable], optional
        :param requires_grad: Whether this variable requires a gradient, defaults to True. If False, we'll not compute the gradients on this variable.
        :type requires_grad: bool, optional
        :param role_description: The role of this variable. We find that this has a huge impact on the optimization performance, and being specific often helps quite a bit!
        :type role_description: str
        """

        if predecessors is None:
            predecessors = []
        
        _predecessor_requires_grad = [v for v in predecessors if v.requires_grad]
        
        if (not requires_grad) and (len(_predecessor_requires_grad) > 0):
            raise Exception("If the variable does not require grad, none of its predecessors should require grad."
                            f"In this case, following predecessors require grad: {_predecessor_requires_grad}")
        
        assert type(value) in [str, bytes, int], "Value must be a string, int, or image (bytes). Got: {}".format(type(value))
        if isinstance(value, int):
            value = str(value)
        # We'll currently let "empty variables" slide, but we'll need to handle this better in the future.
        # if value == "" and image_path == "":
        #    raise ValueError("Please provide a value or an image path for the variable")
        if value != "" and image_path != "":
            raise ValueError("Please provide either a value or an image path for the variable, not both.")

        if image_path != "":
            if is_valid_url(image_path):
                self.value = httpx.get(image_path).content
            else:
                with open(image_path, 'rb') as file:
                    self.value = file.read()
        else:
            self.value = value
            
        self.gradients: Set[Variable] = set()
        self.gradients_context: Dict[Variable, str] = defaultdict(lambda: None)
        self.grad_fn = None
        self.role_description = role_description
        self.predecessors = set(predecessors)
        self.requires_grad = requires_grad
        self._reduce_meta = []
        
        if requires_grad and (type(value) == bytes):
            raise ValueError("Gradients are not yet supported for image inputs. Please provide a string input instead.")
The main thing. Nodes in the computation graph. Really the heart and soul of textgrad.

        :param value: The string value of this variable, defaults to "". In the future, we'll go multimodal, for sure!
        :type value: str or bytes, optional
        :param image_path: The path to the image file, defaults to "". If present we will read from disk or download the image.
        :type image_path: str, optional
        :param predecessors: predecessors of this variable in the computation graph, defaults to None. Here, for instance, if we have a prompt -> response through an LLM call, we'd call the prompt the predecessor, and the response the successor. 
        :type predecessors: List[Variable], optional
        :param requires_grad: Whether this variable requires a gradient, defaults to True. If False, we'll not compute the gradients on this variable.
        :type requires_grad: bool, optional
        :param role_description: The role of this variable. We find that this has a huge impact on the optimization performance, and being specific often helps quite a bit!
        :type role_description: str
        
textgrad/variable.py