Say it again: values not expressions

Wednesday 29 November 2023

Sometimes you can explain a simple thing for the thousandth time, and come away with a deeper understanding yourself. It happened to me the other day with Python mutable argument default values.

This is a classic Python “gotcha”: you can provide a default value for a function argument, but it will only be evaluated once:

>>> def doubled(item, the_list=[]):
...     the_list.append(item)
...     the_list.append(item)
...     return the_list
...
>>> print(doubled(10))
[10, 10]
>>> print(doubled(99))
[10, 10, 99, 99]    # WHAT!?

I’ve seen people be surprised by this and ask about it countless times. And countless times I’ve said, “Yup, the value is only calculated once, and stored on the function.”

But recently I heard someone answer with, “it’s a value, not an expression,” which is a good succinct way to say it. And when a co-worker brought it up again the other day, I realized, it’s right in the name: people ask about “default values” not “default expressions.” Of course it’s calculated only once, it’s a default value, not a default expression. Somehow answering the question for the thousandth time made those words click into place and make a connection I hadn’t realized before.

Maybe this seems obvious to others who have been fielding this question, but to me it was a satisfying alignment of the terminology and the semantics. I’d been using the words for years, but hadn’t seen them as so right before.

This is one of the reasons I’m always interested to help new learners: even well-trodden paths can reveal new insights.

Comments

[gravatar]

This was a bit confusing to read, the first argument is ‘val’ but it appears you are referring to “the value” as “[]”, meaning the default value of “the_list”. Changing the name of the first arg val might be less confusing for the reader, if I understand this article correctly (which I might not lol)

[gravatar]

@Dan, I see what you mean. I changed it from val to item. And you are understanding it correctly!

[gravatar]

I have no idea how I’ve never hit this in my career, but now I’m paranoid about how many ticking time bombs are in my code…

[gravatar]

But recently I heard someone answer with, “it’s a value, not an expression,” which is a good succinct way to say it

Hi, I should say that it’s actually “expression, not value” 😅

Because strictly speaking, default argument gets a value of an expression that is evaluated when the function object is created. Sure, it’s evaluated only once (upon creation and not upon invocations) but it’s still an expression. Note that on the right side of

list = []

is an expression, too.

Here’s an example of default arg which clearly uses an expression:

from operator import mul

def foo(numbers: list = [1, 2, 3] + [4, 5] + mul([6 if True else 7], 4)):
    print(numbers)

foo()

In general it goes like this in language grammar:

default[expr_ty]: '=' a=expression { a } | invalid_default

So technically it’s “expressions, not values” :)

But sure, I understand your point!

[gravatar]

default argument gets a value of an expression

This is tortured. The default argument gets a value which is computed (once!) by an expression. You could also say the default value is a series of characters, which are parsed into an expression, which is evaluated to produce a value. That’s all true, but it doesn’t add anything to the understanding.

The important part is that a function stores a value for the default, it does not store an expression. When people are surprised at Python’s behavior, it’s because they thought the default value was a stored expression which would be evaluated at every call. It’s not. It’s a value.

[gravatar]

When people are surprised at Python’s behavior, it’s because they thought the default value was a stored expression which would be evaluated at every call

Aha, I see what you mean; indeed, I can see how one could think it’s a “stored expression” which should be evaluated upon each invocation. This makes sense!

[gravatar]

How one will fix the example?

[gravatar]

To Newbie

Maybe like this, at each call the_list it’s initialized:

def doubled(item):
    the_list=[]
    the_list.append(item)
    the_list.append(item)
    return the_list


print(doubled(10))

print(doubled(99))

Add a comment:

Ignore this:
Leave this empty:
Name is required. Either email or web are required. Email won't be displayed and I won't spam you. Your web site won't be indexed by search engines.
Don't put anything here:
Leave this empty:
Comment text is Markdown.