You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Covariance is not inferred for NamedTuple and frozen Dataclasses #17623
The new experimental feature NewGenericSyntax (#15238) should infer the variance of type variables. For attributes in NamedTuple and frozen dataclass the variance is inferred as "invariant" when it should be "covariant"
(A clear and concise description of what the bug is.)
To Reproduce
I defined a generic NamedTuple and frozen dataclass with one member of type T. The classes should be covariant in T. So converting from Container[int] to Container[int |None] should be no problem.
It makes sense that variables in a NamedTuple and a frozen dataclass should be considered read-only and should therefore not cause a TypeVar to be inferred as invariant.
Interestingly, Python 3.13's addition of a __replace__ method effectively breaks this. Mypy was recently updated (as was pyright) to synthesize a __replace__ method in NamedTuples and dataclasses. This is done only for Python 3.13 and newer. Building on your playground code sample, the signature of of this synthesized method is:
This __replace__ signature uses T in a contravariant position, which means that the auto-variance algorithm will determine that T is invariant. See this issue for details.
You can see this here in the pyright playground. If you hover over T, you'll see that pyright infers it to be covariant.
However, if the python version is then changed to Python 3.13, its variance changes to invariant, and a type error is reported in the upcast function.
@max-muoto, I'm interested in your thoughts here since you added the support for __replace__ in mypy. I used the same approach in pyright. This variance problem didn't occur to me previously.
Here are some options for us to consider:
We live with the consequences that the synthesized __replace__ always makes any TypeVars used in a NamedTuple or a dataclass invariant starting in Python 3.13.
We add a special-case exemption for __replace__ in the TypeVar variance algorithm. There are already exemptions for __init__ and __new__, so maybe this is acceptable.
We modify the signature for __replace__. I can't think of any way of doing this without making __replace__ unsafe from a type checking perspective. It would probably need to accept parameters of type object and return an object of type Any.
Reacted by Christoph Gietl, Neil Girdhar and Riccardo Bergamaschi
I thought about synthesizing a signature like this:
@dataclass(frozen=True)
class FrozenDataclassBox[T]:
value: T
def __replace__[U = T](self, *, value: U = ...) -> Self: ...
(Note the addition of a default for value. Every parameter to the __replace__ method should be optional.)
Now, if (given fcb: FrozenDataclassBox[bool]) you call fcb.__replace__(), U should get solved to the value of T, so you get a FrozenDataclassBox[bool], which is correct. And if you do fcb.__replace__(value=1), U gets solved to int, and you get a FrozenDataclassBox[int]. I believe this also extends to the general case where there may be multiple fields and multiple type parameters. If the original parameter T has bounds or constraints, then the synthesized type parameter U should have the same bounds or constraints.
It makes sense that variables in a NamedTuple and a frozen dataclass should be considered read-only and should therefore not cause a TypeVar to be inferred as invariant.
Interestingly, Python 3.13's addition of a __replace__ method effectively breaks this. Mypy was recently updated (as was pyright) to synthesize a __replace__ method in NamedTuples and dataclasses. This is done only for Python 3.13 and newer. Building on your playground code sample, the signature of of this synthesized method is:
This __replace__ signature uses T in a contravariant position, which means that the auto-variance algorithm will determine that T is invariant. See this issue for details.
You can see this here in the pyright playground. If you hover over T, you'll see that pyright infers it to be covariant.
However, if the python version is then changed to Python 3.13, its variance changes to invariant, and a type error is reported in the `upcast` function.
@max-muoto, I'm interested in your thoughts here since you added the support for `__replace__` in mypy. I used the same approach in pyright. This variance problem didn't occur to me previously.
Here are some options for us to consider:
We live with the consequences that the synthesized __replace__ always makes any TypeVars used in a NamedTuple or a dataclass invariant starting in Python 3.13.
We add a special-case exemption for __replace__ in the TypeVar variance algorithm. There are already exemptions for __init__ and __new__, so maybe this is acceptable.
We modify the signature for __replace__. I can't think of any way of doing this without making __replace__ unsafe from a type checking perspective. It would probably need to accept parameters of type object and return an object of type Any.
This is definitely something I didn't think of when adding this to dataclasses, but it makes sense that we'd start to hit this. I think special-casing is probably the best path forward here, I obviously don't maintain any type-checkers, but the main concern here that comes to mind is the lack of documentation around this + all of the current special casing for __replace__. MyPy and Pyright will obviously have the correct behavior, and we can create issues for Pyre/PyType, but I would be worried about others implementing type-checkers. I made a cpython issue to document this alongside other dataclass behavior, but not sure that will happen: python/cpython#121371
At least, in my experience, the more I can rely on auto-variance from 3.12 type parameters, the faster I can work with the typing system, so I think making it work correctly by default, even with special casing, is probably the best option for most users.
I think it would be good to define this implicit behavior around synthesizing __replace__ in the typing spec, and also updating the docs for the auto-variance algorithm. Happy to contribute here.
I do like @JelleZijlstra's idea though, I could play around with it in MyPy to see if it might work.
__replace__ breaks more than covariance. It completely breaks type safety in the presence of dataclass subclasses. This passes mypy (playground) and Pyright (playground) but crashes at runtime:
(Of course, B(x=5) would’ve already broken type safety, but that seems like it should be straightforward to fix by adjusting the inferred type of dataclass subclass constructors. The __replace__ problem seems much more difficult, since we can’t tell statically that the subclass is even involved.)
Reacted by Timofei Kukushkin and Shahriar Heidrich
Interestingly, Python 3.13's addition of a replace method effectively breaks this. Mypy was recently updated (as was pyright) to synthesize a replace method in NamedTuples and dataclasses. This is done only for Python 3.13 and newer.
I'm running into this error trying to support Python 3.13 -- how can you suppress these errors? Since it's autogenerated so you can't add a type ignore comment and I checked the PR adding this (#17469) and it seems to add it unconditionally. Defining your own __replace__ doesn't work either -- the Python dataclasses implementation respects a custom __replace__ but the mypy code doesn't.
Interestingly, Python 3.13's addition of a replace method effectively breaks this. Mypy was recently updated (as was pyright) to synthesize a replace method in NamedTuples and dataclasses. This is done only for Python 3.13 and newer.
I'm running into this error trying to support Python 3.13 -- how can you suppress these errors? Since it's autogenerated so you can't add a type ignore comment and I checked the PR adding this (#17469) and it seems to add it unconditionally. Defining your own __replace__ doesn't work either -- the Python dataclasses implementation respects a custom __replace__ but the mypy code doesn't.
I see that issue #18115 was opened addressing this... cross-referencing in case that's helpful...
Hi, it looks like covariance is inferred right for frozen dataclasses, but not for NamedTuple nor frozen attrs dataclasses.
I have to use to this the old way with Generic[T_co] with T_co = TypeVar(..., covariant=True)
Is this being worked on?
Activity
erictraut commentedon Aug 3, 2024
It makes sense that variables in a NamedTuple and a frozen dataclass should be considered read-only and should therefore not cause a TypeVar to be inferred as invariant.
Interestingly, Python 3.13's addition of a
__replace__method effectively breaks this. Mypy was recently updated (as was pyright) to synthesize a__replace__method in NamedTuples and dataclasses. This is done only for Python 3.13 and newer. Building on your playground code sample, the signature of of this synthesized method is:This
__replace__signature usesTin a contravariant position, which means that the auto-variance algorithm will determine thatTis invariant. See this issue for details.You can see this here in the pyright playground. If you hover over
T, you'll see that pyright infers it to be covariant.However, if the python version is then changed to Python 3.13, its variance changes to invariant, and a type error is reported in the
upcastfunction.@max-muoto, I'm interested in your thoughts here since you added the support for
__replace__in mypy. I used the same approach in pyright. This variance problem didn't occur to me previously.Here are some options for us to consider:
__replace__always makes any TypeVars used in a NamedTuple or a dataclass invariant starting in Python 3.13.__replace__in the TypeVar variance algorithm. There are already exemptions for__init__and__new__, so maybe this is acceptable.__replace__. I can't think of any way of doing this without making__replace__unsafe from a type checking perspective. It would probably need to accept parameters of typeobjectand return an object of typeAny.JelleZijlstra commentedon Aug 3, 2024
I thought about synthesizing a signature like this:
(Note the addition of a default for
value. Every parameter to the__replace__method should be optional.)Now, if (given
fcb: FrozenDataclassBox[bool]) you callfcb.__replace__(),Ushould get solved to the value ofT, so you get aFrozenDataclassBox[bool], which is correct. And if you dofcb.__replace__(value=1),Ugets solved toint, and you get aFrozenDataclassBox[int]. I believe this also extends to the general case where there may be multiple fields and multiple type parameters. If the original parameterThas bounds or constraints, then the synthesized type parameterUshould have the same bounds or constraints.However, this is invalid because TypeVar defaults may not use TypeVars from an outer scope (https://typing.readthedocs.io/en/latest/spec/generics.html#scoping-rules) and because the meaning of TypeVar defaults with function-scoped type parameters is unspecified (https://typing.readthedocs.io/en/latest/spec/generics.html#function-defaults). I tried to get around that by using a different type variable, but pyright still doesn't allow it (https://pyright-play.net/?pythonVersion=3.13&strict=true&code=GYJw9gtgBAJghgFzgYwDZwM4YKYagSwgAcwQFZEV0sBYAKFEigQE8j8A7AcwONPMw4yAfVZFs9egAF4SNIIAUjAF7YOAXgAqIAK7YAlPXlYoAMXCqOAEUrGMAITAAPANqaAugC56UX1ABucKh6nlCaknR%2BsNjAUMLCINhE6MjY8W4ATAA0YQDMUOphGe4KOKjAoeZgljZy1A7Ome45AFQ5gcHYoZr5hQB0A-pQALQAfGYWarVUgo6uPV5QA30RMDHMuAhKyABGlZPWtvVzLjtgYKju%2Bt6RfoLYImLY2zt98YnJKGnCCvo5VTUjrNGmcLlcfHcsA8EKI2M9gLs3gkkilvgoOnp1ABGP4TapTIFYE6cBDguhAA), and I don't think its behavior is incorrect. Still, perhaps a type checker could synthesize an internal signature following this approach.
max-muoto commentedon Aug 3, 2024
This is definitely something I didn't think of when adding this to dataclasses, but it makes sense that we'd start to hit this. I think special-casing is probably the best path forward here, I obviously don't maintain any type-checkers, but the main concern here that comes to mind is the lack of documentation around this + all of the current special casing for
__replace__. MyPy and Pyright will obviously have the correct behavior, and we can create issues for Pyre/PyType, but I would be worried about others implementing type-checkers. I made a cpython issue to document this alongside other dataclass behavior, but not sure that will happen: python/cpython#121371At least, in my experience, the more I can rely on auto-variance from 3.12 type parameters, the faster I can work with the typing system, so I think making it work correctly by default, even with special casing, is probably the best option for most users.
I think it would be good to define this implicit behavior around synthesizing
__replace__in the typing spec, and also updating the docs for the auto-variance algorithm. Happy to contribute here.I do like @JelleZijlstra's idea though, I could play around with it in MyPy to see if it might work.
andersk commentedon Oct 21, 2024
__replace__breaks more than covariance. It completely breaks type safety in the presence ofdataclasssubclasses. This passes mypy (playground) and Pyright (playground) but crashes at runtime:(Of course,
B(x=5)would’ve already broken type safety, but that seems like it should be straightforward to fix by adjusting the inferred type ofdataclasssubclass constructors. The__replace__problem seems much more difficult, since we can’t tell statically that the subclass is even involved.)aszs commentedon Oct 29, 2024
I'm running into this error trying to support Python 3.13 -- how can you suppress these errors? Since it's autogenerated so you can't add a type ignore comment and I checked the PR adding this (#17469) and it seems to add it unconditionally. Defining your own
__replace__doesn't work either -- the Python dataclasses implementation respects a custom__replace__but the mypy code doesn't.aszs commentedon Nov 8, 2024
I see that issue #18115 was opened addressing this... cross-referencing in case that's helpful...
wrobell commentedon Jan 10, 2025
I have recently encountered that problem when moving to Python 3.13.
Based on the example from Mypy documentation, to compare equivalent non-dataclass and dataclass code (where error is expected for BoxX example):
https://mypy-play.net/?mypy=latest&python=3.13&flags=strict&gist=8bb34a25c739bfee75b34ce3d6607d65
https://mypy-play.net/?mypy=latest&python=3.12&flags=strict&gist=8bb34a25c739bfee75b34ce3d6607d65
Please note the different errors between implicit (BoxB) and explicit (BoxC) notation.
smheidrich commentedon Aug 20, 2025
For anyone else who comes across this and wants to follow the discussion, it was continued here: https://discuss.python.org/t/make-replace-stop-interfering-with-variance-inference/96092
Ignore plugin-generated members when inferring PEP 695 variance
LoicRiegel commentedon Jul 6, 2026
Hi, it looks like covariance is inferred right for frozen dataclasses, but not for NamedTuple nor frozen attrs dataclasses.
I have to use to this the old way with
Generic[T_co]withT_co = TypeVar(..., covariant=True)Is this being worked on?
Fix variance inference issues caused by dataclass replace
Fix variance inference issues caused by dataclass replace (#21694)