-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Description
Description
Mypy does not recognize type narrowing when validation (via raise if None) is extracted into a separate method. This forces users to duplicate checks locally (e.g., with assert or re-assigning variables) to satisfy the type checker, violating DRY and complicating code refactoring. The issue occurs even though the validation method ensures fields are not None at runtime before proceeding.
This is similar to issues like #11475 (isinstance narrowing ignored in extra functions), but specifically for class methods validating multiple instance fields without passing them as arguments. Workarounds like returning narrowed types or using TypeGuard are cumbersome here, as they require restructuring the code (e.g., returning tuples or protocols), and may introduce bugs (as noted in #16618 for TypeGuard on methods).
It could be an annoying problem when developing classes with non-trivial logic of state changes or patter "Builder".
Minimal Reproducible Example
Here's a minimal example demonstrating the problem. The _validate_fields method raises if any field is None, ensuring they are int afterward. However, mypy does not narrow the types in the calling method.
class TestMypyAbilities:
_field1: int | None
_field2: int | None
_field3: int | None
def __init__(self) -> None:
self._field1 = None
self._field2 = None
self._field3 = None
def _validate_fields(self) -> None:
if self._field1 is None:
raise RuntimeError("field1 is None")
if self._field2 is None:
raise RuntimeError("field2 is None")
if self._field3 is None:
raise RuntimeError("field3 is None")
def some_method(self, x: int) -> int:
self._validate_fields() # Should narrow _field1, _field2, _field3 to int
# Mypy should infer: self._field1: int, etc.
return self._field1 + self._field2 + self._field3 + xActual Behavior
Running mypy demo.py (or mypy --strict demo.py) produces errors, as if narrowing didn't occur:
demo.py:22: error: Unsupported operand types for + ("int" and "None") [operator]
demo.py:22: error: Unsupported operand types for + ("None" and "int") [operator]
demo.py:22: error: Unsupported left operand type for + ("None") [operator]
demo.py:22: note: Both left and right operands are unions
Found 3 errors in 1 file (checked 1 source file)
Possible ad hoc fixes
The simples solution is
def some_method(self, x: int) -> int:
self._validate_fields()
# duplicating validation logics
assert self._field1 is not None
assert self._field2 is not None
assert self._field3 is not None
return self._field1 + self._field2 + self._field3 + xbut this or returning a tuple of narrowed fields from the method) work but add boilerplate and reduce code maintainability.
Expected Behavior
Mypy should recognize the narrowing after the method call and not report operator errors, treating the fields as int post-validation (similar to local if raise statements).
Additional Notes
This was tested with default mypy config; no custom plugins or flags change the behavior.
Versions
- mypy: 1.15.0 (compiled: yes)
- Python: 3.14.2
- OS: Tested on macOS and Linux (Fedora)