I ran into this while trying to enable strict_equality on a codebase.
The problem is the last reveal_type call - Mypy claims the type is still Literal[E.ONE], but it's obviously Literal[E.TWO] (or at least E). If strict_equality is enabled, this will cause a type error when asserting in a test, for example.
Pyright doesn't seem to narrow here, claiming the type of a.a is E always.
I don't know what the correct answer is here. Maybe allow narrowing only on local variables? 🤷
Yeah, this can be a problem. I'm tagging this as feature, since mypy is working "as designed", though here the behavior is not what is expected.
The narrowing behavior has been around for a very long time, but I think the situation got worse when mypy started narrowing enums on assignment. One option would be to experiment with less eagerly narrowing enums to literal types on attribute assignments/assertions/comparisons. Disabling narrowing of attribute access more widely may cause a lot of new errors, so we need to careful there.
Just to add another example (from aiohttp codebase):
if self._at_eof:
return b""
data = bytearray()
while not self._at_eof:
data.extend(await self.read_chunk(self.chunk_size))
if decode: # unreachable error here
return self.decode(data)
return data
Mypy thinks the code is unreachable because it has narrowed the attribute to False and thinks the loop will never exit.
I don't know how complex it'd be to implement, but I'm think the best approach could be to do the type narrowing, but anytime a method is called on that object, or an await happens to yield to the event loop, then the type narrowing should be reset.
So:
assert foo.bar is True
foo.bar # narrowed to True
foo.something()
foo.bar # bool again
Expecting a type checker to know that an arbitrary function modified an attribute seems unreasonable to me. I think this should be limited to methods resetting the narrowing (otherwise the narrowing would be reset for everything, such as a print() call, at which point it become a lot less useful as a feature).
@Dreamsorcerer Honestly, I completely missed the existing responses here, because GH collapsed them.
To have "sound" type narrowing, mypy needs to know which functions are mutating arguments. This information is not available. Your suggestion seems to be that we approximate this information:
if a function is a method, then a function is mutating,
otherwise a function is read-only.
In my opinion, this does more harm than good. Everyone would need to know this new aspect of mypy. Moreover, it introduces other errors (for example read-only methods then be misclassified; and functions, which call mutating methods, would still be misclassified).
I still think it would be good if the incorrect type narrowing was at least opt-out. But if this is not wanted by the maintainers, then we should simply document this edge case, and move on.
classTest:
a: int|None=0defset_a(self, value: int|None) ->None:
self.a=valuea=Test()
reveal_type(a.a) # N: Revealed type is "builtins.int | None"asserta.aisnotNonereveal_type(a.a) # N: Revealed type is "builtins.int"a.set_a(None)
# Oopsreveal_type(a.a) # N: Revealed type is "builtins.int"
This issue is ultimately about mypy inability to invalidate attribute type narrowing when a mutating method is called (or some other potentially mutating event happens, as in #19608).
The nuclear option here is to not narrow fields at all; if the user wants narrowing, they can extract a local variable. That would obviously break a lot of code, but as an opt-in behavior, it might be reasonable.
Activity
JukkaL commented on Oct 8, 2024
Yeah, this can be a problem. I'm tagging this as feature, since mypy is working "as designed", though here the behavior is not what is expected.
The narrowing behavior has been around for a very long time, but I think the situation got worse when mypy started narrowing enums on assignment. One option would be to experiment with less eagerly narrowing enums to literal types on attribute assignments/assertions/comparisons. Disabling narrowing of attribute access more widely may cause a lot of new errors, so we need to careful there.
Dreamsorcerer commented on Oct 26, 2024
Just to add another example (from aiohttp codebase):
if self._at_eof: return b"" data = bytearray() while not self._at_eof: data.extend(await self.read_chunk(self.chunk_size)) if decode: # unreachable error here return self.decode(data) return dataMypy thinks the code is unreachable because it has narrowed the attribute to False and thinks the loop will never exit.
Dreamsorcerer commented on Oct 28, 2024
I don't know how complex it'd be to implement, but I'm think the best approach could be to do the type narrowing, but anytime a method is called on that object, or an await happens to yield to the event loop, then the type narrowing should be reset.
So:
SoloJacobs commented on Feb 16, 2025
I also ran into this, with a slightly different example:
However, I did notice that pyright makes exactly the same mistake.
I guess the type narrowing section should be expanded.
Dreamsorcerer commented on Feb 16, 2025
Expecting a type checker to know that an arbitrary function modified an attribute seems unreasonable to me. I think this should be limited to methods resetting the narrowing (otherwise the narrowing would be reset for everything, such as a print() call, at which point it become a lot less useful as a feature).
SoloJacobs commented on Feb 16, 2025
@Dreamsorcerer Honestly, I completely missed the existing responses here, because GH collapsed them.
To have "sound" type narrowing, mypy needs to know which functions are mutating arguments. This information is not available. Your suggestion seems to be that we approximate this information:
In my opinion, this does more harm than good. Everyone would need to know this new aspect of mypy. Moreover, it introduces other errors (for example read-only methods then be misclassified; and functions, which call mutating methods, would still be misclassified).
I still think it would be good if the incorrect type narrowing was at least opt-out. But if this is not wanted by the maintainers, then we should simply document this edge case, and move on.
update TODO.md
sterliakov commented on Aug 7, 2025
Note that this isn't enum-specific.
https://mypy-play.net/?mypy=latest&python=3.12&gist=9d9394ac88293d89cb56b921b85443c6&flags=strict%2Cwarn-unreachable
This issue is ultimately about mypy inability to invalidate attribute type narrowing when a mutating method is called (or some other potentially mutating event happens, as in #19608).
hterik commented on Sep 2, 2025
Similar issues: #14543 #14010
tamird commented on Apr 3, 2026
The nuclear option here is to not narrow fields at all; if the user wants narrowing, they can extract a local variable. That would obviously break a lot of code, but as an opt-in behavior, it might be reasonable.
hauntsaninja commented on Apr 3, 2026
I believe pyre attempted to have that behaviour, and it worked pretty badly in practice.
There are a couple workarounds, but one good one is a dummy re-assignment var = var will reset all narrowing of var.attr (as mentioned in https://github.com/python/mypy/blob/master/CHANGELOG.md#better-type-narrowing )
hauntsaninja commented on Apr 3, 2026
Closing as a duple of #9457