Loop targets

Tuesday 19 November 2024

I posted a Python tidbit about how for loops can assign to other things than simple variables, and many people were surprised or even concerned:

Sample Python assigning to a dict item in a for loop, same as text below
params = {
    "query": QUERY,
    "page_size": 100,
}

# Get page=0, page=1, page=2, ...
for params["page"] in itertools.count():
    data = requests.get(SEARCH_URL, params).json()
    if not data["results"]:
        break
    ...

This code makes successive GET requests to a URL, with a params dict as the data payload. Each request uses the same data, except the “page” item is 0, then 1, 2, and so on. It has the same effect as if we had written it:

for page_num in itertools.count():
    params["page"] = page_num
    data = requests.get(SEARCH_URL, params).json()

One reply asked if there was a new params dict in each iteration. No, loops in Python do not create a scope, and never make new variables. The loop target is assigned to exactly as if it were an assignment statement.

As a Python Discord helper once described it,

While loops are “if” on repeat. For loops are assignment on repeat.

A loop like for <ANYTHING> in <ITER>: will take successive values from <ITER> and do an assignment exactly as this statement would: <ANYTHING> = <VAL>. If the assignment statement is ok, then the for loop is ok.

We’re used to seeing for loops that do more than a simple assignment:

for i, thing in enumerate(things):
    ...

for x, y, z in zip(xs, ys, zs):
    ...

These work because Python can assign to a number of variables at once:

i, thing = 0, "hello"
x, y, z = 1, 2, 3

Assigning to a dict key (or an attribute, or a property setter, and so on) in a for loop is an example of Python having a few independent mechanisms that combine in uniform ways. We aren’t used to seeing exotic combinations, but you can reason through how they would behave, and you would be right.

You can assign to a dict key in an assignment statement, so you can assign to it in a for loop. You might decide it’s too unusual to use, but it is possible and it works.

Comments

[gravatar]

I think it is a terrible idea. It is not a standard Python idiom and the intent of the code is not obvious without reflection, and good code should not require reflection. Just because you CAN do something doesn’t mean you should.

I’ve written my share of APL code and have seen the end of the slippery slope. It is not pretty.

[gravatar]

I think this is a cool idea. There is an argument that if it’s cool it’s Pythonic. The intent was immediately clear without any reflection other than to acknowledge how cool it is. If it’s quicker, shorter, more readable and reliable I can’t think of any reason not to do it.

I’ve written more than my fair share of Perl and thank God that Python isn’t Perl any more than Python is APL.

[gravatar]

Thank you for the post. I may never use this directly, but I have a deeper understanding of for-loops now.

[gravatar]

I’m surprised by the opposition to this code snippet. Why not utilize the assignment in the for loop to stash the value where you need it? I do agree that the version with page_num is more readable, so that’s good. But it also adds an extra variable and one more line of code. In this case the code is small enough that this won’t matter much. But the extra code does add extra opportunity for errors and bugs to creep in during maintenance and refactoring. I’d say both versions are fine, and are coder preference.

[gravatar]

I like that this is available. Just the other day I needed to add an enumeration in an existing loop and this assignment is perfect.

# from
for x, y in points:
   ...
# to
for i, (x, y) in enumerate(points):
   ...

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.