LeetCode #1120 Maximum Average Subtree (Python)
Non-Local Variables

ans is visible (for reading) inside of helper.  It just can't be changed, although its contents can be modified.

So yes,  ans = [0] is an ugly hack.  You don't need it.  What you need in its place is nonlocal.

def maximumAverageSubtree(self, root: TreeNode) -&gt; float:
    ans = 0

    def helper(node):
        nonlocal ans     # &lt;--- Refer to ans in outer (but not global) scope

        if not node:
            return 0, 0

        left_sum, left_count = helper(node.left)
        right_sum, right_count = helper(node.right)

        ans = max(ans, (left_sum + right_sum + node.val) / (left_count + right_count + 1))

        return left_sum + right_sum + node.val, left_count + right_count + 1

    helper(root)

    return ans


Arbitrary-named, narrowly-scoped recursive function

In this case, you're going to need to get used to it.

Instead of creating a new function, which is visible outside the maximumAverageSubtree function, this helper is only useful to this function.  It does not need to exposed to the outside, so it makes sense to hide it inside.

This is a common paradigm in Python.  Decorators use this all the time.  For example, a @timed decorator has an internal wrapper() function.

def timed(func):

    def wrapper(*argv, **kwargs):
        start_time = time.perf_counter()
        result = func(*argv, **kwargs)
        end_time = time.perf_counter()
        print(func.__name__, "took", end_time - start_time, "seconds")
        return result

    return wrapper


The function shouldn't be arbitrarily named; its name should reflect its purpose.  Here, we are wrapping a call to another function, so wrapper() makes sense.

Above, you have a helper() function.  That probably could be named better.  Maybe process_subtree(node).  But it is "scoped" inside maximumAverageSubtree(), so its name doesn't need to repeat that level of detail.