I've noticed that self parameters in python toolboxes don't actually survive between calls to updateMessages and updateParameters
e.g.
import arcpy
class BrokenTool(object):
def __init__(self) -> None:
self.label = "Tool"
self.description = "My Tool"
self.category = "Useful Tools"
self.paramA = None
self.paramB = 1
return
def getParameterInfo(self) -> list:
p1 = arcpy.Parameter(
displayName="Parameter A",
name="paramA",
datatype="GPString",
parameterType="Required",
direction="Input"
)
return [p1]
def updateParameters(self, parameters: list) -> None:
self.paramA = "Hello World"
parameters[0].value = "paramA set to Hello World"
return
def updateMessages(self, parameters: list) -> None:
self.paramB = 2
parameters[0].setWarningMessage("paramB set to 2")
return
def execute(self, parameters: list, messages: list) -> None:
arcpy.AddMessage(f"{self.paramA=}, expected 'Hello World'")
arcpy.AddMessage(f"{self.paramB=}, expected 2")
return
Will show up in ArcPro when opened like this:

Showing that those assignments did in fact get run, but when you run the tool:

The instance attributes were never updated.
The odd thing is that if you hijack Python's default mutable assignment bug, then it can act as a workaround by storing values across calls in a mutable default list value:
import arcpy
class BrokenTool(object):
def __init__(self) -> None:
self.label = "Tool"
self.description = "My Tool"
self.category = "Useful Tools"
self.paramA = None
self.paramB = 1
return
def getParameterInfo(self) -> list:
p1 = arcpy.Parameter(
displayName="Parameter A",
name="paramA",
datatype="GPString",
parameterType="Required",
direction="Input"
)
return [p1]
def updateParameters(self, parameters: list) -> None:
self.paramA = "Hello World"
self._memory_hack(pA=self.paramA)
parameters[0].value = "paramA set to Hello World"
return
def updateMessages(self, parameters: list) -> None:
self.paramB = 2
self._memory_hack(pB=self.paramB)
parameters[0].setWarningMessage("paramB set to 2")
return
def execute(self, parameters: list, messages: list) -> None:
arcpy.AddMessage(f"{self.paramA=}, expected 'Hello World'")
arcpy.AddMessage(f"{self.paramB=}, expected 2")
arcpy.AddMessage("\nExecuting Memory Hack...")
self.paramA, self.paramB = self._memory_hack()
arcpy.AddMessage(f"{self.paramA=}, expected 'Hello World'")
arcpy.AddMessage(f"{self.paramB=}, expected 2")
return
# This is disgusting, don't do this. It relies on a bug in Python's initialization of default arguments to store state because
# Python toolboxes don't actually update self. variables in updateParameters and updateMessages calls
def _memory_hack(self, pA=None, pB=None,
paramA=[None], paramB=[None]) -> tuple:
""" DO NOT EVER DO THIS """
if pA:
paramA[0] = pA
if pB:
paramB[0] = pB
return (paramA[0], paramB[0])Will return:

I guess this means that the tool object is being constantly rebuilt by the interpreter. Is there a good internal reason for this? There are valid reasons for someone to want to store data in an instance attribute (e.g. an expensive data check or calculation that has to be done during the validation loop, but can pass those validated values on the the execute loop), so why is the Tool object being re-initialized over and over instead of either mutated or re-initialized as a carbon copy of the original object and its __dict__?