Further consideration of this PEP has been deferred until Python 3.8 at the earliest.

Finally, a new standard types.CircuitBreaker type is proposed to decouple an object's truth value (as used to determine control flow) from the value it returns from short-circuited circuit breaking expressions, with the following factory functions added to the operator module to represent particularly common switching idioms:

To force short-circuiting of a circuit breaker without having to evaluate the expression creating it twice, a new operator.short_circuit(obj) helper function will be added to the operator module.

In order to make logical inversion ( not EXPR ) consistent with the above changes, it also proposes the introduction of a new logical inversion protocol (using the method name __not__ ).

Taking advantage of the new protocol, it further proposes that the definition of conditional expressions be revised to also permit the use of if and else respectively as right-associative and left-associative general purpose short-circuiting operators:

Inspired by PEP 335 , PEP 505 , PEP 531 , and the related discussions, this PEP proposes the definition of a new circuit breaking protocol (using the method names __then__ and __else__ ) that provides a common underlying semantic foundation for:

As noted above, PEP 535 is a proposal to build on the circuit breaking protocol defined in this PEP in order to expand the rich comparison support introduced in PEP 207 to also handle comparison chaining operations like LEFT_BOUND < VALUE < RIGHT_BOUND .

However, initial feedback on this PEP indicated that the number of different proposals that it covered made it difficult to read, so that part of the proposal has been separated out as PEP 535 .

PEP 335 proposed the ability to overload the short-circuiting and and or operators directly, with the ability to overload the semantics of comparison chaining being one of the consequences of that change. The proposal in an earlier version of this PEP to instead handle the element-wise comparison use case by changing the semantic definition of comparison chaining is drawn directly from Guido's rejection of PEP 335 [ 1 ].

This means that while the None-aware operators would remain highly specialised and specific to None, other sentinel values would still be usable through the more general protocol-driven proposal in this PEP.

In all three cases, the dedicated syntactic form would be optimised to avoid actually creating the circuit breaker instance and instead implement the underlying control flow directly. In the latter two cases, the syntactic form would also avoid evaluating EXPR twice.

Given the changes proposed by this PEP:

This PEP complements the None-aware operator proposals in PEP 505 , by offering an underlying protocol-driven semantic framework that explains their short-circuiting behaviour as highly optimised syntactic sugar for particular uses of conditional expressions.

This PEP is a direct successor to PEP 531 , replacing the existence checking protocol and the new ?then and ?else syntactic operators defined there with the new circuit breaking protocol and adjustments to conditional expressions and the not operator.

This PEP builds on an extended history of work in other proposals. Some of the key proposals are discussed below.

The circuit breaking protocol ( if-else ) Conditional expressions ( LHS if COND else RHS ) are currently interpreted as an expression level equivalent to: if COND: _expr_result = LHS else: _expr_result = RHS This PEP proposes changing that expansion to allow the checked condition to implement a new "circuit breaking" protocol that allows it to see, and potentially alter, the result of either or both branches of the expression: _cb = COND _type_cb = type(cb) if _cb: _expr_result = LHS if hasattr(_type_cb, "__then__"): _expr_result = _type_cb.__then__(_cb, _expr_result) else: _expr_result = RHS if hasattr(_type_cb, "__else__"): _expr_result = _type_cb.__else__(_cb, _expr_result) As shown, interpreter implementations would be required to access only the protocol method needed for the branch of the conditional expression that is actually executed. Consistent with other protocol methods, the special methods would be looked up via the circuit breaker's type, rather than directly on the instance.

Circuit breaking operators (binary if and binary else ) The proposed name of the protocol doesn't come from the proposed changes to the semantics of conditional expressions. Rather, it comes from the proposed addition of if and else as general purpose protocol driven short-circuiting operators to complement the existing True and False based short-circuiting operators ( or and and , respectively) as well as the None based short-circuiting operator proposed in PEP 505 ( ?? ). Together, these two operators would be known as the circuit breaking operators. In order to support this usage, the definition of conditional expressions in the language grammar would be updated to make both the if clause and the else clause optional: test: else_test ['if' or_test ['else' test]] | lambdef else_test: or_test ['else' test] Note that we would need to avoid the apparent simplification to else_test ('if' else_test)* in order to make it easier for compiler implementations to correctly preserve the semantics of normal conditional expressions. The definition of the test_nocond node in the grammar (which deliberately excludes conditional expressions) would remain unchanged, so the circuit breaking operators would require parentheses when used in the if clause of comprehensions and generator expressions just as conditional expressions themselves do. This grammar definition means precedence/associativity in the otherwise ambiguous case of expr1 if cond else expr2 else expr3 resolves as (expr1 if cond else expr2) else epxr3 . However, a guideline will also be added to PEP 8 to say "don't do that", as such a construct will be inherently confusing for readers, regardless of how the interpreter executes it. The right-associative circuit breaking operator ( LHS if RHS ) would then be expanded as follows: _cb = RHS _expr_result = LHS if _cb else _cb While the left-associative circuit breaking operator ( LHS else RHS ) would be expanded as: _cb = LHS _expr_result = _cb if _cb else RHS The key point to note in both cases is that when the circuit breaking expression short-circuits, the condition expression is used as the result of the expression unless the condition is a circuit breaker. In the latter case, the appropriate circuit breaker protocol method is called as usual, but the circuit breaker itself is supplied as the method argument. This allows circuit breakers to reliably detect short-circuiting by checking for cases when the argument passed in as the candidate expression result is self .

Overloading logical inversion ( not ) Any circuit breaker definition will have a logical inverse that is still a circuit breaker, but inverts the answer as to when to short circuit the expression evaluation. For example, the operator.true and operator.false circuit breakers proposed in this PEP are each other's logical inverse. A new protocol method, __not__(self) , will be introduced to permit circuit breakers and other types to override not expressions to return their logical inverse rather than a coerced boolean result. To preserve the semantics of existing language optimisations (such as eliminating double negations directly in a boolean context as redundant), __not__ implementations will be required to respect the following invariant: assert not bool(obj) == bool(not obj) However, symmetric circuit breakers (those that implement all of __bool__ , __not__ , __then__ and __else__ ) would only be expected to respect the full semantics of boolean logic when all circuit breakers involved in the expression are using a consistent definition of "truth". This is covered further in Respecting De Morgan's Laws.

Forcing short-circuiting behaviour Invocation of a circuit breaker's short-circuiting behaviour can be forced by using it as all three operands in a conditional expression: obj if obj else obj Or, equivalently, as both operands in a circuit breaking expression: obj if obj obj else obj Rather than requiring the using of any of these patterns, this PEP proposes to add a dedicated function to the operator to explicitly short-circuit a circuit breaker, while passing other objects through unmodified: def short_circuit(obj) """Replace circuit breakers with their short-circuited result Passes other input values through unmodified. """ return obj if obj else obj

Circuit breaking identity comparisons ( is and is not ) In the absence of any standard circuit breakers, the proposed if and else operators would largely just be unusual spellings of the existing and and or logical operators. However, this PEP further proposes to provide a new general purpose types.CircuitBreaker type that implements the appropriate short circuiting logic, as well as factory functions in the operator module that correspond to the is and is not operators. These would be defined in such a way that the following expressions produce VALUE rather than False when the conditional check fails: EXPR if is_sentinel(VALUE, SENTINEL) EXPR if is_not_sentinel(VALUE, SENTINEL) And similarly, these would produce VALUE rather than True when the conditional check succeeds: is_sentinel(VALUE, SENTINEL) else EXPR is_not_sentinel(VALUE, SENTINEL) else EXPR In effect, these comparisons would be defined such that the leading VALUE if and trailing else VALUE clauses can be omitted as implied in expressions of the following forms: # To handle "if" expressions, " else VALUE" is implied when omitted EXPR if is_sentinel(VALUE, SENTINEL) else VALUE EXPR if is_not_sentinel(VALUE, SENTINEL) else VALUE # To handle "else" expressions, "VALUE if " is implied when omitted VALUE if is_sentinel(VALUE, SENTINEL) else EXPR VALUE if is_not_sentinel(VALUE, SENTINEL) else EXPR The proposed types.CircuitBreaker type would represent this behaviour programmatically as follows: class CircuitBreaker: """Simple circuit breaker type""" def __init__(self, value, bool_value): self.value = value self.bool_value = bool(bool_value) def __bool__(self): return self.bool_value def __not__(self): return CircuitBreaker(self.value, not self.bool_value) def __then__(self, result): if result is self: return self.value return result def __else__(self, result): if result is self: return self.value return result The key characteristic of these circuit breakers is that they are ephemeral: when they are told that short circuiting has taken place (by receiving a reference to themselves as the candidate expression result), they return the original value, rather than the circuit breaking wrapper. The short-circuiting detection is defined such that the wrapper will always be removed if you explicitly pass the same circuit breaker instance to both sides of a circuit breaking operator or use one as all three operands in a conditional expression: breaker = types.CircuitBreaker(foo, foo is None) assert operator.short_circuit(breaker) is foo assert (breaker if breaker) is foo assert (breaker else breaker) is foo assert (breaker if breaker else breaker) is foo breaker = types.CircuitBreaker(foo, foo is not None) assert operator.short_circuit(breaker) is foo assert (breaker if breaker) is foo assert (breaker else breaker) is foo assert (breaker if breaker else breaker) is foo The factory functions in the operator module would then make it straightforward to create circuit breakers that correspond to identity checks using the is and is not operators: def is_sentinel(value, sentinel): """Returns a circuit breaker switching on 'value is sentinel'""" return types.CircuitBreaker(value, value is sentinel) def is_not_sentinel(value, sentinel): """Returns a circuit breaker switching on 'value is not sentinel'""" return types.CircuitBreaker(value, value is not sentinel)

Truth checking comparisons Due to their short-circuiting nature, the runtime logic underlying the and and or operators has never previously been accessible through the operator or types modules. The introduction of circuit breaking operators and circuit breakers allows that logic to be captured in the operator module as follows: def true(value): """Returns a circuit breaker switching on 'bool(value)'""" return types.CircuitBreaker(value, bool(value)) def false(value): """Returns a circuit breaker switching on 'not bool(value)'""" return types.CircuitBreaker(value, not bool(value)) LHS or RHS would be effectively true(LHS) else RHS

would be effectively LHS and RHS would be effectively false(LHS) else RHS No actual change would take place in these operator definitions, the new circuit breaking protocol and operators would just provide a way to make the control flow logic programmable, rather than hardcoding the sense of the check at development time. Respecting the rules of boolean logic, these expressions could also be expanded in their inverted form by using the right-associative circuit breaking operator instead: LHS or RHS would be effectively RHS if false(LHS)

would be effectively LHS and RHS would be effectively RHS if true(LHS)

None-aware operators If both this PEP and PEP 505's None-aware operators were accepted, then the proposed is_sentinel and is_not_sentinel circuit breaker factories would be used to encapsulate the notion of "None checking": seeing if a value is None and either falling back to an alternative value (an operation known as "None-coalescing") or passing it through as the result of the overall expression (an operation known as "None-severing" or "None-propagating"). Given these circuit breakers, LHS ?? RHS would be roughly equivalent to both of the following: is_not_sentinel(LHS, None) else RHS

RHS if is_sentinel(LHS, None) Due to the way they inject control flow into attribute lookup and subscripting operations, None-aware attribute access and None-aware subscripting can't be expressed directly in terms of the circuit breaking operators, but they can still be defined in terms of the underlying circuit breaking protocol. In those terms, EXPR?.ATTR[KEY].SUBATTR() would be semantically equivalent to: _lookup_base = EXPR _circuit_breaker = is_not_sentinel(_lookup_base, None) _expr_result = _lookup_base.ATTR[KEY].SUBATTR() if _circuit_breaker Similarly, EXPR?[KEY].ATTR.SUBATTR() would be semantically equivalent to: _lookup_base = EXPR _circuit_breaker = is_not_sentinel(_lookup_base, None) _expr_result = _lookup_base[KEY].ATTR.SUBATTR() if _circuit_breaker The actual implementations of the None-aware operators would presumably be optimised to skip actually creating the circuit breaker instance, but the above expansions would still provide an accurate description of the observable behaviour of the operators at runtime.

Rich chained comparisons Refer to PEP 535 for a detailed discussion of this possible use case.

Other conditional constructs No changes are proposed to if statements, while statements, comprehensions, or generator expressions, as the boolean clauses they contain are used entirely for control flow purposes and never return a result as such. However, it's worth noting that while such proposals are outside the scope of this PEP, the circuit breaking protocol defined here would already be sufficient to support constructs like: def is_not_none(obj): return is_sentinel(obj, None) while is_not_none(dynamic_query()) as result: ... # Code using result and: if is_not_none(re.search(pattern, text)) as match: ... # Code using match This could be done by assigning the result of operator.short_circuit(CONDITION) to the name given in the as clause, rather than assigning CONDITION to the given name directly.