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

Summary

I want to propose a language feature that allows a function argument to be conditionally omitted directly at the call site using a bare inline if. The idea aims to provide a concise and natural way to skip passing an argument so that the function default applies, without duplicating calls or relying on boilerplate patterns. I am asking for feasibility and design feedback, and I do not have knowledge of Python grammar rules or CPython implementation details.

The idea

The proposal is that a function argument could be written with a bare inline if. If the condition is true, the value is passed. If it is false, the argument is omitted entirely.

Example:

def fetch_data(user_id: int, timeout: int = 10) -> None:
    """
    Hypothetical API call.
    timeout: network timeout in seconds, defaults to 10
    """
    
timeout: int | None = ...  # Could be an int or None.
user_id: int = 42

fetch_data(
    user_id,
    timeout = timeout if timeout  # passes only if timeout is not None; or if timeout is truthy.
)

Meaning:

  • if timeout evaluates true, the argument is passed
  • otherwise, it is omitted and the function uses the default value 10

This feels conceptually familiar because it matches how we currently write an external if statement that conditionally calls a function with or without the argument. The proposed inline form removes duplication while keeping the intention clear and close to the call site.

I also considered a keyword like omit or a soft keyword, for example:

timeout = timeout if condition else omit

but this becomes more verbose, so I am not suggesting it as the preferred form.

Why this is needed

Current Python patterns for conditionally omitting arguments work, but each has significant drawbacks. None provide a clean or elegant call-site level expression of the intention. The lack of a direct mechanism forces developers to choose between duplication and extra machinery.

Existing workarounds and their weaknesses

1. Duplicating the call with a surrounding if

if timeout:
    return fetch_data(user_id, timeout=timeout)
return fetch_data(user_id)

When multiple optional arguments are involved, this quickly becomes repetitive. It also splits the function call across control flow branches, making it harder to visually parse.

2. Building a temporary kwargs dict

kwargs = {}
if timeout:
    kwargs["timeout"] = timeout
if retries:
    kwargs["retries"] = retries
fetch_data(user_id, **kwargs)

This introduces boilerplate, an extra mutable object, and complicates type checking since the temporary mapping must be typed separately. It also separates the argument from the call site.

3. Inline dict unpacking

fetch_data(
    user_id,
    **({"timeout": timeout} if timeout else {}),
    **({"retries": retries} if retries else {})
)

This is compact but visually noisy. It hides the arguments under an unpacking expression rather than keeping them in their natural keyword positions. Type checkers also have difficulty tracking the eventual keywords.

4. Repeating the default values

fetch_data(
    user_id,
    timeout = timeout if timeout else 10,
    retries = retries if retries else 3
)

This duplicates the default values, which becomes incorrect if the function signature changes. It also breaks the connection between the defaults defined by the function and the defaults used at the call site.

5. Using None as a sentinel and reassigning inside the function

def fetch_data(user_id, timeout=None, retries=None):
    if timeout is None:
        timeout = 10
    if retries is None:
        retries = 3

This shifts responsibility into the function and requires altering the function’s design purely because the call site cannot omit arguments conditionally.

Why the current situation lacks elegance

All existing patterns either duplicate work, obscure intent, introduce unnecessary containers, or disconnect the function’s default from the call site. None directly express the simple intention: omit this argument when a condition fails.

The proposed inline form keeps the argument exactly where it belongs, expresses the logic concisely, and avoids external scaffolding.

Questions for evaluation

I would like feedback on feasibility and design:

  1. Is such inline conditional omission syntactically and conceptually acceptable for Python
  2. Would this create ambiguity or parsing challenges
  3. Can CPython’s grammar support conditional omission without major complexity
  4. Are there fundamental reasons why omission must stay outside the call site rather than being expressed inline
  5. If this concept is workable, could it justify becoming a formal PEP

I am especially interested in whether this fits Python’s design philosophy and whether the parser or call mechanism makes it impractical.

9 Likes

Looks like this:

And also included in this:

Oops! I did not see this in search earlier, I think my keywords were incorrect; I’m sorry for the duplication.

This doesn’t actually explain why conditionally omitting arguments is needed in the first place, it just points out that if you need it, existing solutions are insufficient. You should explain why the feature is beneficial in the first place. Without that, it’s impossible to evaluate whether the drawbacks of the existing solutions are bad enough to warrant new syntax.

Personally, I’ve always found that existing solutions (typically just explicitly providing the default value, or letting None act as an “omitted argument” placeholder) to be perfectly sufficient.

2 Likes

The benefits are:

  1. Natural conditional omission: Allows you to omit arguments at the call site just as if you were using a formal if statement, but without duplicating function calls or branching logic.

  2. In-place, call-site control: Lets you skip certain arguments directly where they are passed, without having to manage them elsewhere or restructure code; everything stays clear and local at the call site.

  3. Safe defaults, no extra patterns: Preserves the function’s defined default values without relying on sentinels or auxiliary patterns, avoiding duplication and potential bugs.

  4. Easy to read and understand: You can instantly see which arguments are being passed and which rely on defaults, even when a function has multiple default parameters (IMO, very readable for caller’s point-of-view).

1 Like

I’ve also “needed” this. When you have a function with some kwargs that need to be passed onto another function, and some of the kwargs are optional, this pattern arises.

I settled on

f(
  **{"arg_name": arg_value if arg_value},
)

as my solution. Which is fine. Just looks a bit silly, and is noicy, and makes renaming arguments more work.

Your code is no valid syntax. Instead something like the following is possible:

f(
  **{"arg_name": arg_value} if arg_value else {},
)

Alternative would be

f(
  **{"arg_name": arg_value for _ in range(1) if arg_value},
)
2 Likes

You’re right,

f(
  **({"arg_name": arg_value} if arg_value else {}),
)

this was indeed the variant I used.

oops. Does demonstrate how annoying this can be :laughing:

3 Likes

I’ve needed this - not a whole lot, but when I did, the **{...} solution is really filthy. It felt a little frustrating there’s no nicer way of doing this. I think the OP presents the case for it nicely so I won’t repeat the arguments here.

3 Likes

I find this rather strange to need, because what ends up happening is you have APIs with different defaults, and then pass along those different defaults to the caller of your function, rather than just have a homogenous API for your users. Your users then also have the default changed from under them by the underlying function changing.

You can also just make your function take user provided **kwargs if you’re trying to hide internal implementation details. Any not provided won’t be in what is passed along.

2 Likes

To be clear, whenever I’ve needed anything like this, arg=None has usually been equivalent to omitting the argument, or at worst I’ve needed to check the default value and specify arg=default.

Are there really enough APIs where omitting an argument is semantically distinct from any way of supplying the argument, to make this worth having dedicated syntax to support? In general I’d consider such functions to be badly designed in the first place.

Which is to say, I’d be really interested in knowing when you needed the **{...} approach. And why you didn’t feel that was working around a bad API design.

I should say that I agree the OP’s presentation of the proposal is very good. It’s just that I feel there is an assumption that it’s self-evident that the need is there, and I’m not sure that’s true.

2 Likes

To me, the current situation actually puts design pressure on functions making their arguments | None = None when not completely necessary - and that’s a pity when it happens.

I’ve seen native code functions with stubs with the stubs not having the default, and I’ve seen complicated defaults that I was scared to “vendor” into my code.

I don’t really have a lot of examples off the top of my head. Like I said, it doesn’t come up often. But maybe because of preemption on the part of library devs :wink:

3 Likes

I’m ambivalent about the typing argument here. I never really agreed that | None and Optional[...] conveyed the same intention, and I feel more pressure to avoid | None than I do to write functions with “optional” arguments. But that’s not really the point I was trying to make here - we chose to go down that route with typing, and I don’t think this proposal is a good way to avoid the consequences of that choice.

For the other cases you mention, it comes down more to “I don’t think this happens often enough to justify new syntax”. And the only real way to get a better feel for whether that’s true is to collect evidence.

TBH, I can’t say I like the proposed syntax:

fetch_data(user_id, timeout=timeout if timeout is not None)

feels pretty verbose[1]. So maybe I’m demanding stronger justification because of that. But I do think we need some level of concrete evidence here.


  1. compared to, say fetch_data(user_id, timeout), which is what we could use if fetch_data treated None as equivalent to omission for the timeout argument ↩︎

This is the one thing I find ChatGPT useful for. I asked it the not even accurate “In Python, has it been suggested to allow if-modifiers for function arguments, like func(x, y if z)?” and it right away found that earlier topic.

Not sure why I asked about positional arguments. Maybe because the one case I know where this bothers me is this itertools recipe:

def repeatfunc(function, times=None, *args):
    "Repeat calls to a function with specified arguments."
    if times is None:
        return starmap(function, repeat(args))
    return starmap(function, repeat(args, times))

Might be nicer to say repeat(args, times if times is not None) just once to cover both cases. Although it would be even nicer if repeat simply supported None. Then it would simply be repeat(args, times) to cover both.

4 Likes

I did ask ChatGPT to check both the internet and the Python discussion forum as an extra check, but it also didn’t find anything (just like me, lol) and instead showed me some PEPs (e.g., PEP 671). This only led to me making a mistake, feeling foolish, and getting embarrassed in front of the core Python developers at my first try :smiling_face_with_tear:

1 Like

It really isn’t a big deal.
Some ideas have been iterated many times and still come up from time to time (often without adding anything new or solving any issues).

I quite like this one.
Although, there are ways to work around it, but current ways are either not as convenient or not as performant as they could be.

However, if there is a push in this direction, it would be nice to expand the scope, so to see how transferable it is and if there are any issues generalising it:

foo(1, 2 if True)
foo(1, b=2 if True)
[1, 2 if True]
{'a': 1, 'b': 2 if True}
obj.attr = 1 if True
# Any others?

I think I have seen a proposal regarding list or/and dict lately.


Also, regarding this specific one, I have made several solutions along the way and ended up not using any of them eventually. e.g. decorator @omit_sentinel_args(NOT_GIVEN), which is just way too expensive.

My current solution is to always have None option or NotGiven standard sentinel, where None is meaningful. But I can only ensure it for functions that I write myself.

5 Likes

For the second and third option of workarounds, I’d add, that those don’t work with positional-only arguments. (Basically just making your point even stronger…).

All in all, reading this, I’m actually surprised this suggestion didn’t already have a PEP made (although PEP 671 is somewhat close). Definitely +1 for me, as this would also work good with ternary operators, and might be worth to be added to dictionaries too (iirc there was a proposal for syntax for optional dictionary keys before).

Edit to the last part: Similar to the response by @dg-pb above, with dictionaries and co, where items can easily be excluded, which is useful in so many cases, that I don’t think I’ll need to list any here.

5 Likes

PEP 671 is kinda the flip side of this proposal. The two would work very nicely together though. You would be able to say in a function call “don’t pass this argument”, and in a function definition “do this if the argument wasn’t passed”. IMO the two should be able to be discussed independently, but they would enhance each other.

This would be new ground for Python though, so far as I know. I cannot think of any programming language that allows you to simply “not pass” an argument. Does anyone know of any prior art? And even more importantly, does anyone know of a language that has discussed this and then NOT implemented such a feature?

Lack of prior art doesn’t inherently mean the idea is bad, of course, but it is certainly a point of interest.

1 Like

This has been discussed here.

1 Like