tuples as function arguments

For convenience, Python constructs a temporary tuple as needed for an assignment statement. Thus, all three of your assignment statements are exactly the same once they reach data movement.

A function call is not an assignment statement; it's a reference mapping. Therefore, the semantics are different.

If you want Python to unpack your tuple into two separate arguments, use the * operator:

f(*(2, 3))

A tuple behaves like an immutable list. The fact that you notate them with parentheses is perhaps confusing, but it's more or less a coincidence - a result of the fact that parentheses are used for grouping things together and reducing ambiguity otherwise.

When you call a function, you're not providing a tuple. You're providing arguments. A tuple can be an argument, but only one - it's just a variable of type tuple.

What you can do is expand a tuple (or a list) into a a series of arguments with this notation:

tup = (2, 3)
f(*tup)
# expand the tuple (2,3) into a series of arguments 2, 3

You can do that with dictionaries as well, except with ** instead of *:

my_dict = {"arg1": 1, "arg2": 2}
f(arg1=my_dict["arg1"], arg2=my_dict["arg2"])
f(**my_dict)   # these are equivalent

On the other hand, functions can take arbitrary numbers of arguments (similar to how other languages do for printf() calls). For example:

def g(*args):
    print("I got this many arguments:", len(args))

Here, if you do type(args), you get tuple, and if you do type(*args), you get an error. This is because, in function headers, the * does the exact opposite: it packs the arguments that were given to the function into a single tuple, so that you can work with them. Consider the following:

g(2, 3)  # 2 arguments, both integers
g((2, 3)) # 1 argument, a tuple
g(*(2, 3)) # 2 arguments, both integers

In short,

  • functions are built in such a way that they take an arbitrary number of arguments
  • The * and ** operators are able to unpack tuples/lists/dicts into arguments on one end, and pack them on the other end
  • individual tuples/lists/dicts are otherwise just individual variables.

The thing is that parens are used for several different things in Python -- for calling functions, for making tuples (it's not just the commas that matter, see the empty tuple ()), for changing evaluation priority in expressions.

In cases where interpreting them is ambiguous (e.g. your example f(2, 3) could be either a function call with two arguments, or a function call with one argument that is a tuple) the language has to make a choice.

If the Python parser was implemented so that it parsed this as a tuple, it would be impossible to have functions with more than one argument. If the Python parser was implemented so that it parsed this as two arguments, it's impossible to pass a literal tuple without the parens.

Clearly the first is a much bigger limitation, so the second was chosen.

Another example is with tuples with one element -- is (1+2) an expression yielding the number 3, or a tuple with one element, 3? Here if it was the second, then it would be impossible to use parens for expressing priority in an expression ((3+4)*5 vs 3+(4*5)). So it was decided to require the comma after the first element for 1-element tuples ((3,)).