Skip to content

BUG: allow BoolOp, In, and NotIn under DataFrame.query/eval parser='python' - #66560

Draft
bzjpst wants to merge 1 commit into
pandas-dev:mainfrom
bzjpst:fix-python-parser-boolop-in
Draft

BUG: allow BoolOp, In, and NotIn under DataFrame.query/eval parser='python'#66560
bzjpst wants to merge 1 commit into
pandas-dev:mainfrom
bzjpst:fix-python-parser-boolop-in

Conversation

@bzjpst

@bzjpst bzjpst commented Jul 31, 2026

Copy link
Copy Markdown
  • Tests added and passing
  • All code checks passed
  • Whatsnew entry added
  • Closes an existing issue — see "Background" below

Background

DataFrame.query() and DataFrame.eval() under parser="python" unconditionally raise NotImplementedError when an expression contains a BoolOp (and/or), In, or NotIn AST node. In practice this means:

  • Any and/or combined expression: a > 0 and b > 0NotImplementedError: 'BoolOp' nodes are not implemented
  • Any membership test: a in b'In' nodes are not implemented
  • Any chained comparison: a < b < c (Python lowers this to BoolOp(And, [a<b, b<c])) → same
  • Any string-column equality on a Series: col == "x" — the parser rewrites Eq to In when either side is a string, so it also fails
  • As a consequence, df.query(...) with a MultiIndex fails on almost anything non-numeric, even when the level name resolves fine

The same expressions work under parser="pandas".

The block came from an overly conservative @disallow(...) set on PythonExprVisitor. BaseExprVisitor already implements visit_BoolOp (pandas/core/computation/expr.py:763) and routes In/NotIn through visit_Compare_rewrite_membership_op_maybe_evaluate_binop (which pushes "in"/"not in" through _maybe_eval in Python space). PandasExprVisitor opts back into those nodes; PythonExprVisitor never did. There is no visitor-level obstacle — only the decorator.

Change

Carve BoolOp, In, NotIn out of the disallow set on PythonExprVisitor. Dict and Not remain disallowed — they are still legitimately unimplemented for that visitor path.

No changes to PandasExprVisitor, the numexpr/python engines, or any resolver code. parser="pandas" is entirely unaffected.

Test-suite fallout (fixed here)

The old restriction was codified in many tests as pytest.raises(NotImplementedError, match="'BoolOp'...") blocks, pytest.raises(NotImplementedError, match="'(Not)?In'...") blocks, and skip_if_no_pandas_parser(parser) guards on TestDataFrameQueryWithMultiIndex. Those assertions are now wrong. This PR:

  • Drops skip_if_no_pandas_parser from tests that pass identically under both parsers: every TestDataFrameQueryWithMultiIndex.*, test_chained_cmp_and_in, test_local_variable_with_in, test_at_inside_string, test_query_with_nested_strings, test_query_with_nested_special_character. (Kept the skip on tests that still legitimately need parser="pandas" for @-prefix locals / backtick quoting / df.attr access.)
  • Flattens if parser == "pandas": <assert> else: pytest.raises(...) splits in test_str_query_method, test_str_list_query_method, test_query_with_string_columns, test_simple_bool_ops, test_bool_ops_with_constants, test_simple_cmp_ops, test_complex_cmp_ops, test_chained_cmp_op, test_simple_in_ops, test_compound_invert_op, test_date_index_query_with_NaT_duplicates — both branches now assert the same correct result.
  • Keeps the 'Not' → NotImplementedError assertion in test_fails_and_or_not (Not remains disallowed).

Net: -167/+58 lines across the two test files.

Verification

Against pandas/tests/frame/test_query_eval.py + pandas/tests/computation/test_eval.py:

11,442 passed · 0 failed · 17 skipped · 43 xfailed · 2 xpassed

The 17 skips are the @-prefix locals / backtick / df.attribute tests that legitimately still require parser='pandas'. Prior-existing xfailed/xpassed counts unchanged.

Notes

  • I could not find an existing pandas-dev/pandas issue describing this behavior. Happy to file one and update the whatsnew entry with the issue number if reviewers prefer that workflow.
  • Whatsnew entry added to v3.1.0.rst under "Other".

Signed: Model B

BaseExprVisitor implements visit_BoolOp and routes In/NotIn through
visit_Compare / _rewrite_membership_op / _maybe_evaluate_binop, but the
@disallow set on PythonExprVisitor was blocking those AST nodes from
ever reaching the base. Carve BoolOp/In/NotIn out of the disallow set;
Dict and Not remain disallowed. Adjust tests that previously asserted
those nodes raise NotImplementedError to assert correct results
instead. MultiIndex queries and boolean/membership expressions now
work under parser='python' as they already did under parser='pandas'.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant