Conditional collection literals

Hello everyone :slightly_smiling_face:

I would like to propose a new syntax feature and am here to gather feedback before writing a PEP about it.

I regularly find myself in a situation where I would like to write a collection literal in which some elements are conditional.
For simplicity, I will focus on lists, but the same idea holds for dicts, sets and tuples.
(I included some examples at the end of this post.)
It would look like this:

my_list = [
    elem_1,
    elem_2 if condition,
    elem_3,
]

where elem_2 is only included in my_list if condition evaluates to True.
Do not confuse this with if-expressions, which will always evaluate to something.

It relates well to comprehensions, where we have a filtering if, e.g.:

[x for x in xs if x > 5]

and takes the form of the guards from cases, which are defined as:

guard ::= "if" named_expression

My best guess at how the same functionality could be achieved with the existing syntax is by constructing the list with ifs-blocks:

my_list = [elem_1]

if condition:
    my_list.append(elem_2)

my_list.append(elem_3)

This approach gets worse the more complex such a literal becomes.

I believe that my example syntax above fits well into python, closes a logical gap and allows for much cleaner code, compared to any other currently valid implementation than can do the same.

Here are the examples for the other collection types:

my_dict = {
    'a': elem_1,
    'b': elem_2 if condition,
    'c': elem_3,
}

my_set = {
    elem_1,
    elem_2 if condition,
    elem_3,
}

my_tuple = (
    'a': elem_1,
    'b': elem_2 if condition,
    'c': elem_3,
)

I am looking forward to reading your feedback :slightly_smiling_face:

18 Likes

There was a previous discussion about this: Conditional elements/arguments

I am sorry, I missed that. But since it is closed and focused more on function calls which are more complicated, maybe we can still have a discussion about this topic?

As far as I understand it, the main point of critique is a real world need for that feature.
But I think having a syntax that makes python code more readable while staying true to the way python already expresses similar things is a good idea. It’s not about the one big thing that suddenly becomes possible. It’s about code being more beautiful in general.

2 Likes

And this is not an option for you?

my_list = [
    elem_1,
    *([elem_2] if condition else []),
    elem_3,
]
5 Likes

Hi max; I am struggling to find a need for it. Do any other languages have conditional literals?
I might be so used to not having the feature that I can’t see how it might be of use.

1 Like

I personaly use this idiom a lot, both in collection displays and function calls. I think it’s the best way to do it to date, but it still looks pretty clunky, so I am really in favour of this proposal!

My typical use case is something like

do_something(
    ...,
    options=[
        option_a,
        option_b,
        *((option_c,) if some_config else ()),
    ],
)

Of course, the options list could be constructed in a separate statement then extended conditionally:

options = [
    option_a,
    option_b,
]
if some_config:
    options.append(option_c)

do_something(
    ...,
    options=options,
)

But that’s quite more verbose and can move code far away, for example if the call has lots of other arguments. It is also problematic for type checking, because options list has to be annotated with the type of the parameter in do_something, which can be complex/impossible to do (I can expand on that if needed).

9 Likes

I find myself wanting this for constructing subprocess arguments. Having a few conditionally set command line options, forcing you down the args = []; if condition: args.append("--flag") path, makes the construction code look so much more complicated than it should be.

2 Likes

You can do something like this:

my_list = [
    elem_1,
    elem_2 if condition else ...,
    elem_3,
]
my_list = [x for x in my_list if x is not ...]

Or use an empty string or None if they make sense. If there’s nothing that makes sense, OMIT = object() will give you a unique sentinel.

9 Likes

This use case illustrated here has come up for me many times over the years. Normally I’m against syntactic sugar because Python has such nice explicit ways of expressing things, but in this case, the syntax presented seems to me to be so much nicer than the alternatives so far given.

The first is what I usually do. It’s a bit dense with symbols, which makes it hard to read. The one with deletion probably needs a comment to say that you’re going to conditionally add in sentinels and then delete them. The syntax presented on the other hand is clear and direct.

It may however be nicer to put the condition first since—while in the examples, the condition sticks out—it may not stick out in general. Something like:

my_dict = {
    'a': elem_1,
    if condition:
        'b': elem_2,
    'c': elem_3,
}

This makes the condition really stick out and it would allow you to group many elements under a single condition. It sacrifices putting everything on one line (I think that’s a small sacrifice).

4 Likes

This looks quite similar to the dart syntax. I wondered myself if it would make sense to add it to Python. Yes there are alternatives, but these are complex compared to a strait forward approach. What I often see happening is something like this instead

my_dict = {
    'a': elem_1,
    'c': elem_3,
}
if condition:
    my_dict['b'] = elem_2

Not only is a at least marginally slower, it might also make it more difficult for type checkers to correctly infer the value type. So you often have to go back and add an explicit annotation just for it. Arguably I’d also say the inline condition would be faster to comprehend in most cases.

4 Likes

elem_2 if condition, is ambiguous, because we can already write elem_2 if condition else 42,.

It doesn’t create the perception that the item won’t be included in the collection. It’s simply code executed at runtime to generate a value, with the false condition resulting in None. Whatever the expression evaluates to will be included in the list.

my_list = [
    elem_1,
    expression,
    elem_3,
]

I think this falls under templates, for example:

my_list = t[
    elem_1,
    "{expression}," if condition else ""
    elem_3,
]

Just a draft, not an actual proposal. I’m not a fan of templates.

Personally I rarely meet this pattern. When I came across a need for this for the first time, I’ve found that unpacking pattern and I stil prefer it - it’s the only way it would work with tuples, because they’re immutable, and it allows to preserve order without adding an awkward insertion at specific index. Though inside a dictionary this pattern does look odd (never used it for a dictionary before).

a = (
    *((42,) if False else ()),
)
print(a) # ()
b = {
    **({"x": 42} if True else {}),
    **({"y": 42} if False else {}),
}
print(b) # {'x': 42}

I like the proposed syntax for the simplicity. What I don’t like about it - it may create a confusion on whether someone actually meant ternary ... if ... else ... or something else. And given how rarely this pattern occur, it may be not worth it.
Another note that adding new syntax that would break backward compatibility to support rare corner case probably won’t be welcomed, so if there are any ideas about how to keep it backward compatible - it might help.

my_set = {
    elem_2 if condition,
    elem_3 if condition else None,
}

Also, shouldn’t this thread be named “Conditional collection elements” for clarity?

2 Likes

I really like this syntax - it’s clear and unambiguous.

That said, I fear it may be considered too difficult to implement (and/or too difficult to teach)…

But if this was being put forward as a PEP, it’d get my +1 easily.

Edit: As below, to be clear, the if condition: would need to be on its own line, and the body of the if would need to be on one or more whole lines, for this to be valid. I don’t support the idea of allowing this syntax to be contracted onto fewer lines than that, as doing so would make it both unclear and unambiguous.

1 Like

Not to me - it puts a thing that looks like a (multi-line) statement inside an expression. If you write it on one line, you get

my_dict = { 'a': elem_1, if condition: 'b': elem_2, 'c': elem_3, }

which, while probably not technically ambiguous, is certainly not clear or easy to read.

11 Likes

So basically, each element (or key value pair) becomes a statement (from prior syntax perspective)?

my_list = [
    element_0,
        element_1, #Syntax error?
    element_2,
    if condition:
        element_3,
]
3 Likes

Sorry, I should have been more clear.

I was thinking that only the multi-line syntax would be allowed - which is why I suspect that ultimately it’d be considered too difficult to implement, since I understand lines are parsed differently based on whether they’re inside at least one level of brackets or not.

I absolutely agree that putting things on a single line shouldn’t be allowed if using this syntax. Also, the single-line version is ambiguous, because you then don’t know how many items are supposed to be inside the if block (whereas in the multi-line syntax, this is determined by how many lines are indented).

1 Like

Yes, this is exactly what I do most of the time in similar situations. It’s probably the best of the alternative suggestions, but it still compromises the elegance of the code. Think of constructing a few nested collections this way… Well, you can avoid the need for the temporary collection by writing a function filtering the items, but that still requires extra wrapping and processing, increases indentation depth and is not self-explanatory. Shall I give examples to illustrate what I mean? Believe it’s obvious…

Having the conditional item syntax would definitely make it much more straightforward to write clean, declarative code. Thus I am all for this idea and the suggested syntax works best for my typical use cases—better than the other proposals so far.

1 Like

I dont think that this is actually correct since conditional expressions actually require an else branch. It is also not ambiguous with the use of the if keyword in comprehensions since they require a for before the if.

1 Like

+1. A lot of code can benefit from this elegant syntax.

I do want to add to the discussion the possibility of generalizing the idea to variable arguments and keyword arguments, so that we don’t have to construct a separate list or dict to unpack into a call to conditionally omit an argument or use the default value, i.e.:

def f(*args):
    ...
f(a, b if condition, c) # f(a, c) if condition is False

and:

def f(option1=1, option2=2):
    ...
f(
    option1=9 if condition, # option1=1 if condition is False
    opiton2=3
)

Also pointing out a typo in the OP that for a tuple, it should be like:

my_tuple = (
    elem_1,
    elem_2 if condition,
    elem_3,
)

and that the parenetheses can be omitted in umambiguous contexts as usual:

my_tuple = elem_1, elem_2 if condition, elem_3
3 Likes

There’s a shorter way:

my_list = [
    elem_1,
    * [elem_2]*condition,
    elem_3,
]

This will achieve much the same thing, provided condition is an actual bool and not just a truthy thing, and provided you don’t mind evaluating elem_2 even when it’s not used.

7 Likes