Dual functionalities of min() and max() builtins

Currently, the only way to reliably call max() is with an iterable, and one must work around the issue by adding a level of indirection using a new function:

def max_indirected(*args, **kwargs):
    return max(args, **kwargs)

Without this sort of solution, things get complicated when one attempts to unpack multiple (possibly empty) lists and use the unpacked lists as arguments to max():

list1, list2, list3 = [], [1], []    # results from queries or an API or whatever
max(*list1, *list2, *list3)    # max of all items in these lists, except this doesn't work as expected

Instead, one must work around this issue using something like max_indirected() above or the following

max(max(lst, default=0) for lst in (list1, list2, list3))

Then you have the case where you are using unpacking and want a single list to be treated as the unit of comparison with the key, not as an iterable containing multiple values:

list4 = [1, 2]
lists = [list4]
# return list of the longest length,
# except this calls max([1, 2], key=len),
# and 'int' cannot be used with len().
max(*lists, key=len)

# Workaround:
lists.append([])
max(*lists, key=len)

I’d call this a violation of “simple is better than complex” because max(1) should work the same way max(1, 2) currently does, and max([1], key=func) should behave like max([1], [2], key=func).
That is a much more intuitive behavior.

In my mind, a good solution would be to separate the max(iterable[, default, key]) and max(arg1, *args[, key]) forms, perhaps deprecating the former with a movement into the itertools module and retaining the latter in the builtins module.
Note that the same solution should be also applied in the case of the min() function since it suffers from the same issue.

I dispute this premise. If it’s possible to unexpectedly have only a single argument, you can simply add a shim to ensure that you will always have at least two. Or two shims, if you’re worried you might have zero arguments. Using float(“-inf”) will work for numeric arguments, or create your own custom object that’s less than everything else if you need to be able to test if that’s what you got back.

1 Like

I’d personally consider using positional unpacking in function calls code smell, apart from a few exceptions, like forwarding arguments from one function to another. While I agree that the ambiguity in the 1 positional parameter case is kind of bad and confusing, I also don’t think you are going to find a lot of support for a breaking change in builtin functions over a very small improvement here.

In all of these cases I just wouldn’t use unpacking, since it’s less efficient, I’d use itertools.chain for the first example and for the second example I would just would pass in lists directly without unpacking it and then it works as expected.

1 Like

Using the same multiple-unpacking trick into an iterable also works and I’d call it simpler: max((*list1, *list2, *list3)).

Because you already consciously built a list of values to compare, simply don’t unpack it.

I would have said “special cases aren’t special enough to break the rules” and “in the face of ambiguity, refuse the temptation to guess”, personally. It’s simple enough, and plenty of beginners who didn’t have a clever use case were taught to use it successfully just fine. But I agree with you. It’s fundamentally the same design wart as, say, the issue with trying to format a single value with %-style formatting and finding out it’s a tuple. And honestly it’s weird to me how rarely I see (or think about) this complaint.

More generally, some older “practicality beats purity” interfaces start to look silly or expose needless limitations, as other features get added (like all the generalizations we have of unpacking syntax now).

The problem is, this proposes an ultimately breaking change to functionality that’s about as core as it gets.

I notice that both times in the above discussion, I advised fixing the problem by making it use the max(iterable, *[, default, key]) form, and I think that probably leads to simpler code on average. It’s also what you described as the “reliable” way in the opening to the post, so.

But either way, this kind of deprecation is probably a non-starter, as much of a point as you have. It sounds like a 4.0 change to me.

1 Like