PEP 377: Skipping `with` statement body -- Reimagination

TL;DR

Python lacks other languages’ abilities to create custom conditional block managers without dörty hacks.

The Problem

PEP-377: Allow __enter__() methods to skip the statement body was proposed and declined a long time ago in a galaxy far away in 2009. Since then, many things changed, including some roots of the Python design philosophy.

Thus, I want to restart the discussion on the original post.
The biggest benefit this PEP could give is the ability to make custom conditional statements.

Use Case

This is helpful when wrappers with conditional body execution are used. These wrappers could have on-enter action(s), on-exit action(s), and on-skip action(s) – similar behaviour to the test methods in unittest package.

Example

Suppose we have the following structure to occur multiple times across the codebase:

if (some_condition()):
    print("Step 1: Executing code...")
    try:
        conditional_body()
    except:
        print("Step 1: Oops, something got wrong")
        raise
    else:
        print("Step 1: Done")
else:
    print("Step 1: Skipped")

The internal try-except-else block could be extracted to the context manager level, but the outer if-else could not unless you use the forbidden technique.

On Groovy

This is possible in many other languages, so here’s an implementation on Groovy:


// This can actually be a simple function instead of class
class ConditionalStep implements Serializable
{
    def String name
    def boolean condition
    
    def call(Closure body)
    {
        if (condition)
        {
            println "$name..."
            try
            {
                // If necessary, can provide access to this "ContextManager" via body.setDelegate(this)
                body.call()
            }
            catch (e)
            {
                println "$name: FAILED"
                throw e
            }
            println "$name: OK"
        }
        else
            println "$name: SKIPPED"
    }
    
    @Override
    def String toString()
    { "${this.class.simpleName}(name='$name', condition=$condition)" }
}

// or function which internally creates class
def conditionalStep(Map args, Closure body)
{ new ConditionalStep(args).call(body) }

new ConditionalStep(name: "Step 1", condition: a < b).call 
{
    conditionalBody()
}

// or, if defined as function and not class:
conditionalStep(name: "Step 1", condition: a < b)
{
    conditionalBody()
}

Current Solutions

ContextManager (desired) way

Currently possible only via call stack manipulation.

with CondiditionalStep(name="Step 1", condition=a < b):
    conditional_body()

For-loop way

The second option works better but does support returning the context manager as part of with ... as syntax, and looks weirder.

for _ in ConditionalStep(name="Step 1", condition=a < b):
    conditional_body()

ContextManager If-else way

with ConditionalStep(name="Step 1", condition=a < b) as step:
    if step.ok:
        conditional_body()

Lambdas or functions as arguments way

This works basically on pure Python but looks very ugly and can mess up the local variables namespace.

def step_body():
    conditional_body_part_one()
    conditional_body_part_two()
    # ...

ConditionalStep(name="Step 1", condition=a < b).run(step_body)

I believe there are other use cases and/or implementations for this functionality, so feel free to discuss.

This is the biggest problem with this proposal - I’ve never seen this pattern in actual code, so I find it very hard to believe that this is an important issue to solve. If you could give actual examples of real, public code that uses this pattern, that would be far more helpful than a made-up example.

Honestly, this looks fine to me. You don’t explain what’s bad about it, so I don’t know why it isn’t a suitable solution to the problem.

It’s not at all obvious how you expect this to map to your original example, but if I assume that what you mean is

def step_body():
    print("Step 1: Executing code...")
    try:
        conditional_body()
    except:
        print("Step 1: Oops, something got wrong")
        raise
    else:
        print("Step 1: Done")


if (some_condition()):
    step_body()
else:
    print("Step 1: Skipped")

then I really don’t see what’s wrong with that - it’s a classic example of refactoring a complex conditional statement body into its own function, and it seems perfectly acceptable to me.

So no, I’m -1 on the proposal as it seems like it adds nothing that can’t already be done in multiple perfectly acceptable ways.

6 Likes

You may want to have a look at PEP 343 and its explanation of concerns about obscuring control flow.