Conditional omission of function arguments at the call site using a bare inline if

I’d have much more use for the conditional list/dict elements variant than this one. Being able to write something like:

subproces.run([
    docker,
    "run",
    "--rm",
    "-i" if interactive,
    "-t" if os.isatty(sys.stdout),
    "--userns=keep-id" if docker_variant == "podman",
    "--ulimit=nofile=1024:1048576" if docker_variant == "docker",
    image,
    *command,
])

would simplify a lot of code for me. On the other hand, function(x=something if condition else None) is rarely not enough.

I agree with the sentiment that an argument should always have an easily accessible default. It makes wrapper functions or code that has to pass that argument’s value around before using it awkward if extra conditionals are needed everywhere to handle the argument unset case.

13 Likes

If it’s possible, it would be great to have a single syntax that works for positional arguments, keyword arguments, list displays, and dict displays. If not, so be it (the proposal shouldn’t be stalled out on account of not being able to get the full suite), but IMO the four subvariants aren’t in competition.

5 Likes

I think a syntax most people would agree on is fn(arg if cond) and for keyword (-only) arguments fn(name=arg if cond).

For collections, just elem if cond would be enough too (imo), and key: elem if cond for dictionaries. The ternary operators is obviously similar, but not too clear see to directly cause confusion, and it should be (somewhat) simple to implement this, as it is not too big of a change (some new grammar + this functionality).

The main problem now would be to discuss why this would not be a good idea, and, as you said in your post before, wether other languages have argued about some similar feature, and what decisions were made.

Two small problems I would be able to think of, are following:

  1. It’s hard to read the value some arg will take. For somr fn(arg if cond) it’s hard to directly be able to see what fn will take as arguments, if cond is untrue. Obviously IDEs would be able to help. Also, type-checkers might be able to know that if cond is not truthy, the value will be a Literal[<default>] (if the default is able to be passed to Literal, else just it’s type.
  2. It might get confused with the ternary operator. As I said above, I’m not too sure if this really is that much of an issue, but it might one later on.

Its also worth noting, that errors would need to be rewritten in order to be able to provide a good debugging experience.

The wired thing about this syntax (imo), is that it both helps readability, but also makes it a little worse, by being very similar to the ternary operator.

However, this allows us to not need any new symbols / keywords for this syntax, but only some grammar. Generally, I think we can try and solve all four cases you talked about at once, and also make a concise syntax.

1 Like

Oh, and I though about another workaround. Doing fn(arg or default) works if our cond is just bool(arg). The same problem that you gave for 3 arises though, being that we need to repeat the default value(s) outside the function.

That’s why I liked the proposal I linked above to use pass as a placeholder value. Applied to the omission of function argument, it actually becomes a ternary operator: fetch_data(user_id, timeout=timeout if timeout is not None else pass). It reduces confusion by being more explicit, at the cost of additional verbosity. It reuses an existing keyword which also is a noop, so the same kind of information is conveyed.

Here is the link again: Playing With the Idea of `pass` as an Expression

Oh yeah, I remember that proposal (why does saying that make me feel old). That post got abandoned for some reason, but I really like that idea still. It should also be possible to add that for lists/dicts, and it works with both keyword-only and positional arguments.

I suppose implementing that as the syntax should work too, and avoid confusion.

I heard some people talk about the idea of replacing pass I that expression with something else (e.g. -), and I’m not too sure about that, but I suppose we can make a poll later on.

As for the idea of return ... if ... else pass, that was talked about in the topic, I’m not too sure that that should also be added to this idea, nor should the optional raising errors be added (yet?).

Apart from those posts, I’d love to carry the ideas from that topic into this one.

Yeah, this one is weird but follow the proposal of the other thread to transform pass to a builtin value. I’d be more in favor to still use pass as a syntax construct but in ways that looks like an expression (like in the ternary operator), even if it can be confusing in other ways.

I think pass can be made a soft keyword, similar to type. type can be called, have attribute access, be used in type-hints and so on, but it can also be used as a keyword for type x = y.

So I guess making pass something similar should be possible.

In this case, isn’t the suggestion to make pass do something special? E.g. if it was called, or had attribute access, it might do nothing at all. If pass is downgraded to a soft keyword, that allows people to override it, increasing confusion around the potential syntax. Feels counter-productive to me.

1 Like

That is the goal, but the general idea is to make this syntax just be an addition to ternary operators.

expr if cond else default and expr if cond else pass are basically the same thing, just that the default here is pass, and it is treated like a builtin constant, like type is too (although type can be called, have attribute access, …). But generally, the idea is the same, just that type is more versatile than pass would / should be.

My understanding is that value if condition is not an expression, but ‘syntactic sugar’ for *([value] if condition else []). That makes that value if condition cannot occur where an expression is expected, only where also star expressions are allowed, i.e. function arguments and set/list/dict displays.

Now it seems that the ternary value if condition else pass is positioned as alternative approach that is an expression. Is that understanding correct? And if so, what would be the value of pass? I.e. what does below output?

a = 1 if False else pass
print(a)
4 Likes

Exactly, that’s the fatal flaw of all make pass be an expression ideas. Python does already have something that can be used in any expression to indicate the absence of a value: None. But since None can be used in any expression, it can be stored in a list. [1, 2 if False else None, 3] has obviously a length of 3. So, since None is an expression, it actually fails somehow in indicating the absence of a value. Making pass be yet another sentinel value would be actually counterproductive.

2 Likes

In my view, if pass isn’t the direction taken, we could introduce a new keyword like omit.
For example: expr if condition else omit or even expr omit if condition.
I think putting omit in front of if has a style similar to how lazy goes in front of import in Python 3.15, and I prefer it over if ... else omit. I also find omit if more Pythonic, and it clearly shows that it’s different from the normal ternary if ... else expression.

But introducing a whole new keyword (omit) or keyword combination (omit if) only for function arguments doesn’t seem fully justified.
If the same keyword or keyword combination could also be used for omitting items in collections, then it would feel more reasonable, because it wouldn’t be limited just to function arguments but would have a broader purpose.

Edit: ah, now I think about it, omit if condition would make nonsense like it’s negation of condition.

The if-guards in comprehensions like [i*i for i in range(10) if i%2 ==0] have no else pass and no omit. This is the status quo.

Why should it be different for displays (like possibly [1, 2 if spam(), 3]) or function calls (print(1, 2 if ham(), 3))?

2 Likes

Thinking about it now, I think you are right. I did think about the syntactical ambiguouity of expr if cond, but thinking about it, doing expr if cond else pass is even worse.

So I suppose expr if cond would be the preferred way, as it should be able to work the same as the other idea.

1 Like

What would return ... if ... else pass mean?

Well, return ... if ... else ... is equivalent to:

if ...:
    return ...
else:
    return ...

so it would be equivalent to:

if ...:
    return ...
else:
    return pass

and return pass would presumably be equivalent to return, which is the same as return None.

Therefore, return ... if ... else pass would be equivalent to return ... if ... else None, which we can already do!

1 Like

return pass would be equivalent to pass.

1 Like

Indeed.
If return pass means return None, why wouldn’t you write return None? And similarly return … if … else None?

2 Likes

If return pass means pass, what does return 1 + pass mean? Which would result from return 1 + (… if False else pass) IIUC

Treating omission as if it is an expression invariably leads to inconsistencies. The only way I see is an if without else as syntactic sugar for a star expression in the context of argument lists and list/set/tuple/dict displays.

1 Like

return someexpr if cond else pass would mean to return the evaluation of someexpr. If it is used as a part of another expression, that’s invalid syntax.

Alternatively, return something <operator> expr if cond else passcould mean that we return something unless cond is truey in which case we return something <operator> expr.

I’m generally more in favor of the first option though.

A (somewhat) interesting question also arises; How can this interact with other syntax, e.g. the walrus operator. return x := expr if x else pass could be useful in some cases, although I’d consider it to both be bad code design, and syntactic sugar for something that can easily be expressed in 2 lines, and is not as common.