Skip to content

fix __bool__ check should look at Never return #1021#2966

Draft
asukaminato0721 wants to merge 2 commits intofacebook:mainfrom
asukaminato0721:1021
Draft

fix __bool__ check should look at Never return #1021#2966
asukaminato0721 wants to merge 2 commits intofacebook:mainfrom
asukaminato0721:1021

Conversation

@asukaminato0721
Copy link
Copy Markdown
Contributor

Summary

Fixes #1021

tightening the __bool__ guard.

The check still allows a raw Never value in boolean position, but it now rejects a callable __bool__ whose return type is Never

Test Plan

add test

Copilot AI review requested due to automatic review settings March 30, 2026 12:56
@meta-cla meta-cla Bot added the cla signed label Mar 30, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Tightens Pyrefly’s truthiness checking so __bool__ implementations that never return (i.e., return type Never) are rejected in boolean contexts, aligning behavior with the expectation in issue #1021.

Changes:

  • Update check_dunder_bool_is_callable to additionally error when a callable __bool__ has return type Never.
  • Add a regression test covering def __bool__(...) -> Never used in an if condition.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
pyrefly/lib/alt/attr.rs Adds a Never-return check for callable __bool__ attributes during truthiness validation.
pyrefly/lib/test/simple.rs Adds a regression testcase asserting an error when __bool__ returns Never.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pyrefly/lib/alt/attr.rs
Comment on lines +2586 to +2589
if dunder_bool_ty
.callable_return_type(self.heap)
.is_some_and(|ret| ret.is_never())
{
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

callable_return_type only works for top-level callable types (Function/BoundMethod/Callable/Overload). Since callability is checked via as_call_target, dunder_bool_ty can still be callable via __call__ (e.g., an instance/class/protocol with __call__) but then callable_return_type returns None and this Never-return guard won’t fire. If the intent is to reject any callable __bool__ whose call result is Never, consider extracting the return type from the CallTarget (or doing a no-arg call_infer with swallowed errors) rather than relying on Type::callable_return_type.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown

Diff from mypy_primer, showing the effect of this PR on open source code:

pandera (https://github.com/pandera-dev/pandera)
+ ERROR pandera/engines/pandas_engine.py:2045:16-49: The `__bool__` method of `Series[bool]` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandera/engines/pandas_engine.py:2071:16-49: The `__bool__` method of `Series[bool]` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandera/engines/pyarrow_engine.py:438:12-45: The `__bool__` method of `Series[bool]` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandera/engines/pyarrow_engine.py:463:12-45: The `__bool__` method of `Series[bool]` returns `Never`, so it cannot be used as a boolean [invalid-argument]

freqtrade (https://github.com/freqtrade/freqtrade)
+ ERROR freqtrade/strategy/interface.py:1372:12-65: The `__bool__` method of `Series[bool]` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR freqtrade/strategy/interface.py:1376:13-1379:50: The `__bool__` method of `Series[bool]` returns `Never`, so it cannot be used as a boolean [invalid-argument]

pandas (https://github.com/pandas-dev/pandas)
+ ERROR pandas/core/arrays/base.py:592:12-42: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/arrays/base.py:1534:12-28: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/arrays/interval.py:1054:12-28: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/arrays/masked.py:1104:13-80: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/arrays/masked.py:1122:12-44: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/arrays/sparse/array.py:860:12-28: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/arrays/string_.py:491:16-30: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/arrays/string_.py:508:16-34: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/arrays/string_.py:573:16-30: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/arrays/string_.py:588:16-73: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/dtypes/cast.py:1406:8-53: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/dtypes/cast.py:1413:12-27: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/frame.py:12439:16-26: The `__bool__` method of `Series` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/generic.py:10013:12-19: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/groupby/generic.py:2851:20-38: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/groupby/groupby.py:6010:14-76: The `__bool__` method of `Series` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/groupby/ops.py:675:21-49: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/indexes/multi.py:3508:12-40: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/internals/construction.py:438:19-60: The `__bool__` method of `Index` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/internals/construction.py:438:19-60: The `__bool__` method of `Series` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/internals/construction.py:972:16-59: The `__bool__` method of `Index` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/missing.py:614:12-39: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/nanops.py:358:20-40: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/core/window/ewm.py:366:16-38: The `__bool__` method of `Series` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/io/formats/style.py:1808:20-25: The `__bool__` method of `Series` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/io/formats/style.py:1808:20-39: The `__bool__` method of `Series` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/io/formats/style.py:1833:20-25: The `__bool__` method of `Series` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/io/formats/style.py:1833:20-39: The `__bool__` method of `Series` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/io/json/_normalize.py:515:16-33: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/io/stata.py:505:16-34: The `__bool__` method of `Series` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/io/stata.py:1918:20-62: The `__bool__` method of `Series` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/tests/apply/test_frame_apply.py:760:10-45: The `__bool__` method of `NDFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/tests/frame/methods/test_reindex.py:348:16-35: The `__bool__` method of `Series` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/tests/frame/test_constructors.py:2265:16-29: The `__bool__` method of `DataFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/tests/frame/test_constructors.py:2265:16-29: The `__bool__` method of `Series` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/tests/generic/test_frame.py:53:17-21: The `__bool__` method of `DataFrame` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/tests/generic/test_series.py:49:17-25: The `__bool__` method of `Series` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/tests/generic/test_series.py:57:17-25: The `__bool__` method of `Series` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/tests/generic/test_series.py:65:17-25: The `__bool__` method of `Series` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/tests/groupby/methods/test_rank.py:496:8-18: The `__bool__` method of `Series` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/tests/groupby/test_reductions.py:1168:12-38: The `__bool__` method of `Series` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/tests/resample/test_datetime_index.py:1355:8-28: The `__bool__` method of `Series` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/tests/resample/test_datetime_index.py:1380:8-28: The `__bool__` method of `Series` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR pandas/tests/test_col.py:351:13-26: The `__bool__` method of `Expression` returns `Never`, so it cannot be used as a boolean [invalid-argument]

optuna (https://github.com/optuna/optuna)
+ ERROR optuna/storages/_rdb/alembic/versions/v3.0.0.c.py:158:17-161:73: The `__bool__` method of `ColumnElement[bool]` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR optuna/storages/_rdb/alembic/versions/v3.0.0.c.py:167:17-168:77: The `__bool__` method of `ColumnElement[bool]` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR optuna/storages/_rdb/alembic/versions/v3.0.0.d.py:162:16-69: The `__bool__` method of `ColumnElement[bool]` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR optuna/storages/_rdb/alembic/versions/v3.0.0.d.py:166:16-70: The `__bool__` method of `ColumnElement[bool]` returns `Never`, so it cannot be used as a boolean [invalid-argument]
+ ERROR optuna/storages/_rdb/storage.py:665:20-85: The `__bool__` method of `ColumnElement[bool]` returns `Never`, so it cannot be used as a boolean [invalid-argument]

@github-actions
Copy link
Copy Markdown

Primer Diff Classification

❌ 4 regression(s) | 4 project(s) total | +53 errors

4 regression(s) across pandera, freqtrade, pandas, optuna. error kinds: invalid-argument, ColumnElement __bool__ returns Never. caused by callable_return_type(), is_never().

Project Verdict Changes Error Kinds Root Cause
pandera ❌ Regression +4 invalid-argument pyrefly/lib/alt/attr.rs
freqtrade ❌ Regression +2 invalid-argument pyrefly/lib/alt/attr.rs
pandas ❌ Regression +42 invalid-argument callable_return_type()
optuna ❌ Regression +5 ColumnElement __bool__ returns Never is_never()
Detailed analysis

❌ Regression (4)

pandera (+4)

These are false positives caused by a type inference issue. The expression data_container.dtype == self.type first accesses the .dtype property of data_container (typed as PandasObject), which returns a dtype object (e.g., pd.ArrowDtype), and then compares it with self.type (also a pd.ArrowDtype | None). This comparison between two dtype objects returns a plain Python bool. However, pyrefly appears to infer the result as Series[bool], likely because data_container is typed as PandasObject (a union that includes pd.Series, pd.DataFrame, and pd.Index) and pyrefly resolves the == operator through the Series __eq__ overload rather than correctly recognizing that the comparison is between dtype objects. Since pd.Series.__bool__ is typed as returning Never (it raises ValueError for ambiguous truth value), the new check in the PR fires. But the actual runtime type of the comparison result is bool, not Series[bool], so these errors are incorrect. The Never in the error messages is a downstream consequence of pyrefly's incorrect type inference for the == expression.
Attribution: The change in pyrefly/lib/alt/attr.rs adds a new check: when __bool__ is callable but its return type is Never, pyrefly now emits an error. The root cause is that pyrefly incorrectly infers the type of data_container.dtype == self.type as Series[bool] (because data_container is typed as PandasObject which includes pd.Series, and == on a Series returns Series[bool]). The pandas Series.__bool__ method is typed as returning Never (it raises ValueError to prevent ambiguous truth testing). However, in this code, the == comparison is between two dtype objects (not Series), so the result is actually a plain bool. Pyrefly's type inference incorrectly resolves the comparison result as Series[bool], and then the new __bool__ returning Never check triggers a false positive.

freqtrade (+2)

These are false positives caused by an inaccurate return type annotation on get_latest_candle, which declares its return as tuple[DataFrame | None, datetime | None]. At runtime, dataframe.loc[...].iloc[-1] returns a Series (a single row), so .get(SignalType.ENTER_LONG, 0) == 1 produces a plain bool. However, pyrefly follows the declared return type and treats latest as a DataFrame. On a DataFrame, .get() returns a Series, and Series == 1 produces Series[bool]. The pandas Series[bool].__bool__ is typed to return Never (an intentional design to prevent ambiguous truth-value testing on multi-element Series), so pyrefly flags the use of Series[bool] in boolean context. The code is correct at runtime because the actual type is a scalar bool, not Series[bool]. The fix would be to correct the return type annotation of get_latest_candle to tuple[Series | None, datetime | None], or pyrefly could handle the __bool__ -> Never pattern more leniently for pandas types. Neither mypy nor pyright typically flag this pattern in practice.
Attribution: The change in pyrefly/lib/alt/attr.rs added a new check: when __bool__ is callable but its return type is Never, pyrefly now emits an error saying the type cannot be used as a boolean. This new check fires on Series[bool] because pandas stubs define Series.__bool__ as returning Never. The issue is that pyrefly infers the type of enter_long, exit_long, etc. as Series[bool] when they are actually scalar bool values at runtime (since latest is a row from .iloc[-1]). The new callable_return_type check in attr.rs then flags these as invalid boolean usage.

pandas (+42)

The new check is too aggressive when applied to union types. Pyrefly's map_over_union examines each member of a union independently, so when isna() returns bool | NDFrame (or similar), it flags the NDFrame member's __bool__() -> Never even though the actual value at these call sites is always a bool. The 42 errors are all false positives — the code being flagged uses scalar boolean values, not NDFrame objects. Neither mypy nor pyright flag any of these locations.
Attribution: The change to AnswersSolver in pyrefly/lib/alt/attr.rs added a new check: when dunder_bool_ty.[callable_return_type()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/attr.rs) returns Never, pyrefly now emits an invalid-argument error. This new check fires on union types where one member (NDFrame) has __bool__() -> Never, even when the actual value at runtime would be a different union member (like bool). The check in the map_over_union callback examines each union member independently, so it flags NDFrame even when the code path would never produce an NDFrame.

optuna (+5)

ColumnElement bool returns Never: All 5 errors flag SQLAlchemy ColumnElement[bool] objects being used in boolean contexts (if statements). SQLAlchemy's type stubs define ColumnElement.__bool__ as returning Never to prevent accidental boolean evaluation of SQL expressions. Pyright also flags all 5 of these. While the runtime behavior may differ (ORM instances have mapped values, not column descriptors), the static type information correctly indicates __bool__ returns Never. This is a correct application of the new check.

Overall: This is a genuinely correct new check — ColumnElement[bool].__bool__ does return Never in SQLAlchemy's type stubs, and pyright agrees these are errors. The check itself is sound: if __bool__ returns Never, using the object in a boolean context would always raise. The fact that pyright also flags all 5 errors strengthens the case that this is a real type-level issue. While at runtime these specific code paths work because ORM instances have different attribute types than class descriptors (a SQLAlchemy typing limitation), from a static typing perspective the errors are correct given the type information available. The PR intentionally tightened the __bool__ guard to reject callables whose return type is Never, and these are legitimate catches of that pattern.

Attribution: The change in pyrefly/lib/alt/attr.rs added a new check: when __bool__ is callable but its return type is Never, pyrefly now emits an error. Previously, pyrefly only checked if __bool__ was not callable. The new code at lines adding callable_return_type(self.heap).is_some_and(|ret| ret.[is_never()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/attr.rs)) is what triggers these errors. SQLAlchemy's ColumnElement.__bool__ is typed as returning Never (to signal it raises), so this new check catches it.

Suggested fixes

Summary: The new __bool__() -> Never check in attr.rs fires incorrectly on union types because map_over_union checks each member independently, causing false positives when only some union members have __bool__() -> Never (e.g., bool | NDFrame, Series[bool] inferred from union-typed receivers).

1. In the closure f passed to self.[map_over_union()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/attr.rs) in pyrefly/lib/alt/attr.rs (around line 2590), add a guard so that the __bool__() -> Never check is only emitted when the entire original type (not just a single union member) has __bool__() -> Never. Specifically, when checking callable_return_type().is_some_and(|ret| ret.[is_never()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/attr.rs)), skip emitting the error if the original type_of_term_used_as_bool is a union type and the current union_member_ty is only one branch of that union. In pseudo-code: before the callable_return_type check, add if type_of_term_used_as_bool.[is_union()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/attr.rs) { return; } — or more precisely, only emit the __bool__() -> Never error when the union member being checked is the same as the full type (i.e., it's not a union, or every member of the union has __bool__() -> Never). This prevents false positives where bool | NDFrame is used in boolean context — the bool member is fine, so the overall expression should be fine.

Files: pyrefly/lib/alt/attr.rs
Confidence: high
Affected projects: pandas, pandera, freqtrade
Fixes: invalid-argument
The map_over_union callback checks each union member independently. For types like bool | NDFrame (from isna()) or bool | Series[bool], the NDFrame/Series member has __bool__() -> Never, but the bool member is perfectly valid in boolean context. The check should not fire when at least one union member has a valid __bool__. This would eliminate all 42 pandas errors (all pyrefly-only) and likely the 4 pandera errors and 2 freqtrade errors where the inferred type is a union containing both a scalar bool path and a Series/NDFrame path. The optuna errors (which are co-reported by pyright and involve non-union ColumnElement[bool]) would correctly remain.


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (4 LLM)

Copy link
Copy Markdown
Contributor

@stroxler stroxler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this makes sense, I'll try to get a second review and merge. Thanks!

@meta-codesync
Copy link
Copy Markdown
Contributor

meta-codesync Bot commented Apr 8, 2026

@stroxler has imported this pull request. If you are a Meta employee, you can view this in D100049632.

@fangyi-zhou
Copy link
Copy Markdown
Contributor

It's interesting why isna is triggering new errors for pandas.

  • ERROR pandas/core/arrays/base.py:592:12-42: The __bool__ method of NDFrame returns Never, so it cannot be used as a boolean [invalid-argument]

https://github.com/pandas-dev/pandas/blob/1bba48f900275af485e457b056f3e4fc82b38f94/pandas/core/arrays/base.py#L592

  • ERROR pandas/core/arrays/interval.py:1054:12-28: The __bool__ method of NDFrame returns Never, so it cannot be used as a boolean [invalid-argument]

https://github.com/pandas-dev/pandas/blob/1bba48f900275af485e457b056f3e4fc82b38f94/pandas/core/arrays/interval.py#L1054

Looking at the definition of isna https://github.com/pandas-dev/pandas/blob/main/pandas/core/dtypes/missing.py
It looks like we're getting back an union type with one of the return types with invalid __bool__ check.

@fangyi-zhou
Copy link
Copy Markdown
Contributor

class Foo:
    def __bool__(self):
        raise TypeError
class Bar(Foo):
    def __bool__(self) -> bool:
        return True
bar: Foo = Bar()
if bar: ...   # <-- We're reporting a false positive here

I managed to produce a false positive error here, maybe we shall tighten the check?

Copy link
Copy Markdown
Contributor

@fangyi-zhou fangyi-zhou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review automatically exported from Phabricator review in Meta.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

__bool__ check should look at Never return

4 participants