Skip to content

Warn when class instance without __bool__ or __len__ is used as boolean condition#3282

Open
knQzx wants to merge 5 commits intofacebook:mainfrom
knQzx:warn-instance-always-truthy
Open

Warn when class instance without __bool__ or __len__ is used as boolean condition#3282
knQzx wants to merge 5 commits intofacebook:mainfrom
knQzx:warn-instance-always-truthy

Conversation

@knQzx
Copy link
Copy Markdown
Contributor

@knQzx knQzx commented May 1, 2026

Summary

closes #3282 closes #868

adds ConditionRedundantReason::InstanceAlwaysTruthy - fires when a class instance is used in a boolean context but the class defines neither __bool__ nor __len__ (so instances are unconditionally truthy)

class NoBool:
    pass

x = NoBool()
if x:   # warning: NoBool defines neither __bool__ nor __len__, instances are always truthy
    pass

skipped to keep noise down

  • object itself
  • protocols and ABCs (extends_abc) - concrete subclasses can define the dunders at runtime
  • custom metaclasses inheriting from ABCMeta via MRO (covers HA's ABCCachedProperties(CachedProperties, ABCMeta))
  • classes from bundled stubs (typeshed stdlib + third-party stubs) - many stub-only classes have dynamic runtime behavior not in stubs (datetime, asyncio.Future, Lock, sqlalchemy Session, etc)
  • dataclasses
  • classes with __bool__ / __len__ / __getattr__ / __getattribute__ / __get__ anywhere in MRO (descriptors, dynamic attribute interception)

remaining warnings on home-assistant primer

23 warnings, three patterns:

  • dead defensive checks (~16): device = random.choice(...) returns non-optional T then code does if device:. dead by types, lint working as intended
  • subclass narrowing (~2): base declares attr: T | None, subclass redeclares attr: T. after redeclaration if obj.attr: is dead by types
  • lying types via # type: ignore[assignment] (~5): platform: EntityPlatform = None # type: ignore[assignment]. attr annotated T but assigned None with the assignment error suppressed. if self.platform: is meaningful at runtime but dead by declared types. proper fix is T | None. not suppressed because doing so needs initializer tracking on ClassField to hide what are arguably real type-annotation bugs

none look like the "incomplete stubs" failure mode the issue author flagged

Test Plan

tests in flow_branching.rs cover plain classes, inheritance, descriptors, __getattr*__, dataclasses, stub-only classes, protocols, ABCs, custom-metaclass-via-MRO

cargo test -p pyrefly clean

@meta-cla meta-cla Bot added the cla signed label May 1, 2026
@github-actions github-actions Bot added the size/m label May 1, 2026
@github-actions

This comment has been minimized.

…heck

Variables typed as object, Hashable, Iterable, or any ABC subclass
may hold concrete runtime instances that define __bool__ or __len__.
Skip the warning for those abstract/protocol types.
@github-actions github-actions Bot added size/m and removed size/m labels May 1, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added size/m and removed size/m labels May 1, 2026
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

- include __getattribute__ and __get__ in dunder check (descriptors,
  dynamic attribute interception)
- skip classes from bundled stubs (typeshed stdlib, third-party stubs):
  datetime, asyncio.Future/Lock, sqlalchemy Session etc. have runtime
  behavior not modeled in stubs
- skip dataclasses to avoid noise on the common defensive-guard pattern
- add regression test covering descriptors, __getattr*__, dataclasses
  and stdlib stub types
@github-actions github-actions Bot added size/l and removed size/m labels May 1, 2026
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

extends_abc only matched abc.ABCMeta directly, missing frameworks like
HA's ABCCachedProperties(CachedProperties, ABCMeta) where ABCMeta is
inherited rather than used as the metaclass directly. Walk the metaclass
class's MRO to handle this case
@github-actions github-actions Bot added size/l and removed size/l labels May 1, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

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

core (https://github.com/home-assistant/core)
+ ERROR homeassistant/components/derivative/sensor.py:430:20-29: Instance of `State` used as condition, but `State` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/gdacs/sensor.py:103:12-25: Instance of `GdacsFeedEntityManager` used as condition, but `GdacsFeedEntityManager` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/home_connect/coordinator.py:165:28-49: Instance of `HomeConnectApplianceCoordinator` used as condition, but `HomeConnectApplianceCoordinator` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/knx/storage/config_store.py:254:12-22: Instance of `KNXModule` used as condition, but `KNXModule` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/media_player/__init__.py:1349:16-31: Instance of `EntityPlatform` used as condition, but `EntityPlatform` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/netatmo/media_source.py:122:20-25: Instance of `BrowseMediaSource` used as condition, but `BrowseMediaSource` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/netatmo/media_source.py:130:20-25: Instance of `BrowseMediaSource` used as condition, but `BrowseMediaSource` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/onvif/device.py:175:12-23: Instance of `EventManager` used as condition, but `EventManager` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/owntracks/device_tracker.py:156:12-21: Instance of `HomeAssistant` used as condition, but `HomeAssistant` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/screenlogic/util.py:40:12-36: Instance of `ConfigEntry` used as condition, but `ConfigEntry` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/shelly/diagnostics.py:35:16-33: Instance of `ShellyBlockCoordinator` used as condition, but `ShellyBlockCoordinator` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/solarman/sensor.py:273:16-40: Instance of `ConfigEntry` used as condition, but `ConfigEntry` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/sonos/media_browser.py:324:8-25: Instance of `SonosFavorites` used as condition, but `SonosFavorites` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/sql/config_flow.py:121:8-12: Instance of `Session` used as condition, but `Session` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/sql/config_flow.py:139:12-16: Instance of `Session` used as condition, but `Session` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/sql/config_flow.py:147:16-20: Instance of `Session` used as condition, but `Session` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/sql/config_flow.py:155:8-12: Instance of `Session` used as condition, but `Session` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/sql/util.py:51:16-26: Instance of `HomeAssistant` used as condition, but `HomeAssistant` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/stream/__init__.py:582:12-15: Instance of `HlsStreamOutput` used as condition, but `HlsStreamOutput` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/tts/__init__.py:1343:12-27: Instance of `EntityPlatform` used as condition, but `EntityPlatform` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/components/vegehub/__init__.py:98:12-23: Instance of `VegeHubCoordinator` used as condition, but `VegeHubCoordinator` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/helpers/entity.py:1564:12-25: Instance of `EntityPlatform` used as condition, but `EntityPlatform` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]
+ ERROR homeassistant/helpers/entity.py:1705:25-89: Instance of `PlatformData` used as condition, but `PlatformData` defines neither `__bool__` nor `__len__`, so instances are always truthy. It's equivalent to `True` [redundant-condition]

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

Primer Diff Classification

✅ 1 improvement(s) | 1 project(s) total | +23 errors

1 improvement(s) across core.

Project Verdict Changes Error Kinds Root Cause
core ✅ Improvement +23 redundant-condition class_has_bool_or_len()
Detailed analysis

✅ Improvement (1)

core (+23)

These 23 warnings are technically correct — the flagged class instances are indeed always truthy because their classes define neither __bool__ nor __len__. The PR author analyzed all 23 and confirmed they represent dead defensive checks, subclass narrowing, or lying types via # type: ignore[assignment]. The check has thoughtful exclusions for ABCs, protocols, stubs, dataclasses, and dynamic attribute classes. While this is a novel check not present in mypy/pyright, it correctly identifies genuinely redundant conditions. This is a new lint capability, not a false positive generator.
Attribution: The changes in pyrefly/lib/alt/expr.rs (adding ConditionRedundantReason::InstanceAlwaysTruthy and the Type::ClassType match arm in the condition redundancy checker) directly cause these 23 new warnings. The class_has_bool_or_len() method in pyrefly/lib/alt/attr.rs provides the MRO-walking logic, and metaclass_extends_abcmeta() in pyrefly/lib/alt/class/class_metadata.rs ensures ABCMeta-derived metaclasses are properly excluded.


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (1 LLM)

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.

[feature] warn on using classes that don't define __bool__ in a conditional

1 participant