Skip to content

Commit a1864d4

Browse files
authored
Add error code for mutable covariant override (#16399)
Fixes #3208 Interestingly, we already prohibit this when the override is a mutable property (goes through `FuncDef`-related code), and in multiple inheritance. The logic there is not very principled, but I just left a TODO instead of extending the scope of this PR.
1 parent bc591c7 commit a1864d4

File tree

5 files changed

+90
-3
lines changed

5 files changed

+90
-3
lines changed

docs/source/error_code_list2.rst

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,37 @@ Example:
482482
def g(self, y: int) -> None:
483483
pass
484484
485+
.. _code-mutable-override:
486+
487+
Check that overrides of mutable attributes are safe
488+
---------------------------------------------------
489+
490+
This will enable the check for unsafe overrides of mutable attributes. For
491+
historical reasons, and because this is a relatively common pattern in Python,
492+
this check is not enabled by default. The example below is unsafe, and will be
493+
flagged when this error code is enabled:
494+
495+
.. code-block:: python
496+
497+
from typing import Any
498+
499+
class C:
500+
x: float
501+
y: float
502+
z: float
503+
504+
class D(C):
505+
x: int # Error: Covariant override of a mutable attribute
506+
# (base class "C" defined the type as "float",
507+
# expression has type "int") [mutable-override]
508+
y: float # OK
509+
z: Any # OK
510+
511+
def f(c: C) -> None:
512+
c.x = 1.1
513+
d = D()
514+
f(d)
515+
d.x >> 1 # This will crash at runtime, because d.x is now float, not an int
485516
486517
.. _code-unimported-reveal:
487518

mypy/checker.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2041,7 +2041,6 @@ def check_method_override_for_base_with_name(
20412041
pass
20422042
elif isinstance(original_type, FunctionLike) and isinstance(typ, FunctionLike):
20432043
# Check that the types are compatible.
2044-
# TODO overloaded signatures
20452044
self.check_override(
20462045
typ,
20472046
original_type,
@@ -2056,7 +2055,6 @@ def check_method_override_for_base_with_name(
20562055
# Assume invariance for a non-callable attribute here. Note
20572056
# that this doesn't affect read-only properties which can have
20582057
# covariant overrides.
2059-
#
20602058
pass
20612059
elif (
20622060
original_node
@@ -2636,6 +2634,9 @@ class C(B, A[int]): ... # this is unsafe because...
26362634
first_type = get_proper_type(self.determine_type_of_member(first))
26372635
second_type = get_proper_type(self.determine_type_of_member(second))
26382636

2637+
# TODO: use more principled logic to decide is_subtype() vs is_equivalent().
2638+
# We should rely on mutability of superclass node, not on types being Callable.
2639+
26392640
# start with the special case that Instance can be a subtype of FunctionLike
26402641
call = None
26412642
if isinstance(first_type, Instance):
@@ -3211,14 +3212,28 @@ def check_compatibility_super(
32113212
if base_static and compare_static:
32123213
lvalue_node.is_staticmethod = True
32133214

3214-
return self.check_subtype(
3215+
ok = self.check_subtype(
32153216
compare_type,
32163217
base_type,
32173218
rvalue,
32183219
message_registry.INCOMPATIBLE_TYPES_IN_ASSIGNMENT,
32193220
"expression has type",
32203221
f'base class "{base.name}" defined the type as',
32213222
)
3223+
if (
3224+
ok
3225+
and codes.MUTABLE_OVERRIDE in self.options.enabled_error_codes
3226+
and self.is_writable_attribute(base_node)
3227+
):
3228+
ok = self.check_subtype(
3229+
base_type,
3230+
compare_type,
3231+
rvalue,
3232+
message_registry.COVARIANT_OVERRIDE_OF_MUTABLE_ATTRIBUTE,
3233+
f'base class "{base.name}" defined the type as',
3234+
"expression has type",
3235+
)
3236+
return ok
32223237
return True
32233238

32243239
def lvalue_type_from_base(

mypy/errorcodes.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,12 @@ def __hash__(self) -> int:
255255
"General",
256256
default_enabled=False,
257257
)
258+
MUTABLE_OVERRIDE: Final[ErrorCode] = ErrorCode(
259+
"mutable-override",
260+
"Reject covariant overrides for mutable attributes",
261+
"General",
262+
default_enabled=False,
263+
)
258264

259265

260266
# Syntax errors are often blocking.

mypy/message_registry.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
6363
INCOMPATIBLE_TYPES_IN_ASSIGNMENT: Final = ErrorMessage(
6464
"Incompatible types in assignment", code=codes.ASSIGNMENT
6565
)
66+
COVARIANT_OVERRIDE_OF_MUTABLE_ATTRIBUTE: Final = ErrorMessage(
67+
"Covariant override of a mutable attribute", code=codes.MUTABLE_OVERRIDE
68+
)
6669
INCOMPATIBLE_TYPES_IN_AWAIT: Final = ErrorMessage('Incompatible types in "await"')
6770
INCOMPATIBLE_REDEFINITION: Final = ErrorMessage("Incompatible redefinition")
6871
INCOMPATIBLE_TYPES_IN_ASYNC_WITH_AENTER: Final = (

test-data/unit/check-errorcodes.test

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,3 +1148,35 @@ main:3: note: Revealed local types are:
11481148
main:3: note: x: builtins.int
11491149
main:3: error: Name "reveal_locals" is not defined [unimported-reveal]
11501150
[builtins fixtures/isinstancelist.pyi]
1151+
1152+
[case testCovariantMutableOverride]
1153+
# flags: --enable-error-code=mutable-override
1154+
from typing import Any
1155+
1156+
class C:
1157+
x: float
1158+
y: float
1159+
z: float
1160+
w: Any
1161+
@property
1162+
def foo(self) -> float: ...
1163+
@property
1164+
def bar(self) -> float: ...
1165+
@bar.setter
1166+
def bar(self, val: float) -> None: ...
1167+
baz: float
1168+
bad1: float
1169+
bad2: float
1170+
class D(C):
1171+
x: int # E: Covariant override of a mutable attribute (base class "C" defined the type as "float", expression has type "int") [mutable-override]
1172+
y: float
1173+
z: Any
1174+
w: float
1175+
foo: int
1176+
bar: int # E: Covariant override of a mutable attribute (base class "C" defined the type as "float", expression has type "int") [mutable-override]
1177+
def one(self) -> None:
1178+
self.baz = 5
1179+
bad1 = 5 # E: Covariant override of a mutable attribute (base class "C" defined the type as "float", expression has type "int") [mutable-override]
1180+
def other(self) -> None:
1181+
self.bad2: int = 5 # E: Covariant override of a mutable attribute (base class "C" defined the type as "float", expression has type "int") [mutable-override]
1182+
[builtins fixtures/property.pyi]

0 commit comments

Comments
 (0)