From c3c91594e3d2b0d9b55dffe683e4042e8650098f Mon Sep 17 00:00:00 2001 From: Abanoub Doss Date: Wed, 24 Jun 2026 08:48:36 -0500 Subject: [PATCH] feat(expressions): add truthiness semantics for constant boolean expressions AlwaysTrue and AlwaysFalse now define __bool__ so they evaluate to True and False in control flow. All other BooleanExpressions raise TypeError on bool() to prevent ambiguous truthiness checks: negation is the ~ operator and expressions should be compared explicitly. --- pyiceberg/expressions/__init__.py | 21 ++++++++++++++++ tests/expressions/test_expressions.py | 35 +++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/pyiceberg/expressions/__init__.py b/pyiceberg/expressions/__init__.py index ef4cb2506e..c6e0f49e73 100644 --- a/pyiceberg/expressions/__init__.py +++ b/pyiceberg/expressions/__init__.py @@ -70,6 +70,19 @@ def __or__(self, other: BooleanExpression) -> BooleanExpression: return Or(self, other) + def __bool__(self) -> bool: + """Reject ambiguous truthiness checks on a non-constant expression. + + Truthiness is reserved for control flow, not expression construction. Use ``~expr`` + to negate an expression (``not expr`` would coerce to a Python ``bool``), and compare + explicitly (e.g. ``expr == AlwaysFalse()``). Only :class:`AlwaysTrue` and + :class:`AlwaysFalse` define a truth value. + """ + raise TypeError( + f"The truth value of {type(self).__name__} is ambiguous. Use ~expr to negate an " + "expression and compare explicitly; only AlwaysTrue() and AlwaysFalse() support truthiness." + ) + @model_validator(mode="wrap") @classmethod def handle_primitive_type(cls, v: Any, handler: ValidatorFunctionWrapHandler) -> BooleanExpression: @@ -455,6 +468,10 @@ def __invert__(self) -> AlwaysFalse: """Transform the Expression into its negated version.""" return AlwaysFalse() + def __bool__(self) -> bool: + """Return ``True``; AlwaysTrue is the constant true expression.""" + return True + def __str__(self) -> str: """Return the string representation of the AlwaysTrue class.""" return "AlwaysTrue()" @@ -473,6 +490,10 @@ def __invert__(self) -> AlwaysTrue: """Transform the Expression into its negated version.""" return AlwaysTrue() + def __bool__(self) -> bool: + """Return ``False``; AlwaysFalse is the constant false expression.""" + return False + def __str__(self) -> str: """Return the string representation of the AlwaysFalse class.""" return "AlwaysFalse()" diff --git a/tests/expressions/test_expressions.py b/tests/expressions/test_expressions.py index 8ce48a6897..b5603e8d70 100644 --- a/tests/expressions/test_expressions.py +++ b/tests/expressions/test_expressions.py @@ -1390,6 +1390,41 @@ def test_deepcopy_then_pickle() -> None: # |__/ |__/ +def test_always_true_is_truthy() -> None: + assert bool(AlwaysTrue()) is True + # Usable directly in control flow. + assert "taken" == ("taken" if AlwaysTrue() else "skipped") + + +def test_always_false_is_falsy() -> None: + assert bool(AlwaysFalse()) is False + assert "skipped" == ("taken" if AlwaysFalse() else "skipped") + + +def test_inverted_constants_have_expected_truth_value() -> None: + assert bool(~AlwaysTrue()) is False + assert bool(~AlwaysFalse()) is True + + +@pytest.mark.parametrize( + "expression", + [ + EqualTo("x", 1), + NotEqualTo("x", 1), + IsNull("x"), + NotNull("x"), + In("x", (1, 2)), + And(EqualTo("x", 1), EqualTo("y", 2)), + Or(EqualTo("x", 1), EqualTo("y", 2)), + Not(EqualTo("x", 1)), + ], +) +def test_non_constant_expression_truth_value_is_ambiguous(expression: BooleanExpression) -> None: + # Truthiness is reserved for control flow; ``~expr`` is the negation operator. + with pytest.raises(TypeError, match="truth value of .* is ambiguous"): + bool(expression) + + def _assert_literal_predicate_type(expr: LiteralPredicate) -> None: assert_type(expr, LiteralPredicate)