Conditional context manager (with foo() if bar)

A conditional context manager is created by adding if EXPR1 after with EXPR2, indicating that context manager EXPR2 only applies when EXPR1 evaluates to True, while the indented block will always be executed.

with foo() if cond:
    # Some long suite here...

Here foo() will only be executed and apply to suite when cond is True.

Without this feature, the work around will be:

def bar():
    # Suite here
if cond:
    with foo():
        bar()
else:
    bar()

This is especially useful when there are multiple context managers. Without this syntax, the function will be embedding.

Additionally, with foo() as bar if cond else alt will assign alt to bar if cond is False. The else alt part only works when as presents.

2 Likes

contextlib.ExitStack works very well for handling multiple context managers that you may or may not want to enter.

Say you have 3 context managers, spam, eggs and ham. You can do this:

with contextlib.ExitStack() as stack:
    if condition_1:
        stack.enter_context(spam())
    if condition_2:
        stack.enter_context(eggs())
    if condition_3:
        stack.enter_context(ham())
    # Do the stuff that you need to do here
    # All context managers on the stack are cleaned up at the end of the `with` block

contextlib.nullcontext is also worth checking out.

11 Likes

If it’s very common to use this context manager in this way you could also just build it into the logic of the context manager to not do anything, then it becomes:

with foo(cond):
    # Some long suite here...
3 Likes

And if you have just the one, with foo() if cond else contextlib.nullcontext(): works well.

11 Likes

The proposed syntax looks readable and easy enough to be syntaxic sugar for precisely that. It would also have a very light performance improvement, turning an attribute access and a function call into a noop.
Not to mention the other solutions, which have the merit of existing but are unwieldy for something that simple.

1 Like

Measure before deciding whether that’s even relevant :slight_smile: It may well not make enough performance difference to even detect.

However, this only applies to context managers where we don’t care about their return values.

with foo() as bar if cond:
    # what does bar point to when cond is false?
    bar.do_something()

We should avoid such syntax sugars whose use case is narrow and make users confuse. Instead, it would be better to add a helper function to contextlib, which would look like:

with contextlib.conditional(foo(), cond):
    do_something()

The conditional() always returns None so users can’t use the return value anyway.

5 Likes

When I started reading and saw the proposed syntax I assumed that it would mean “context manager block is skipped when if statement is false”, so I was surprised ehan I saw that it would mean “context manager is not entered but block still executes”. I don’t think any of these cases are used enough to warrant special syntax, and as @frostming pointed out, this would not work with context managers that return something, which is the only case I can think of where I would use this. Specifically, I use this pattern somewhat often:

from pathlib import Path

p = Path("foo")

if p.exists():
    with open(p, "r") as f:
        ...

So in my opinion, a with filter, if added, should filter the entire block and not just the context manager.

Actually, I’ve mentioned this form with spam as eggs if cond else alt in the first post, and I think the else branch should be required when as presents.

There are such cases where context managers are just doing something trivial, such as changing some variables temporarily or tracking some changes caused by the indented block, where we always want the block executed.

Besides, if the whole block is skipped when condition is False, with a if cond, b: would be hard to explain.

Aha, I completely missed the last paragraph (reading comprehension is hard :/). That would however allow this funky syntax:

with foo() if test1() else bar() as baz if test3() else baq():
    # @_@ what context manager am I using?

I think @hauntsaninja has the correct solution for your problem.

I’d even go as far to say it’s likely to degrade performance if the branch is unpredictable, but probably not noticably so in a realistic benchmark.

Good point. But we could make it a syntaxic sugar only of the no-as with statement. And if you want the context manager object accessible as a variable, you have to manage the conditionality yourself (using one of the existing aformentioned objects or functions, the inline if with contextlib.nullcontext() for example).