Correct mutable version of typing.Mapping

AFAICT, when we want covariance the suggestion is to use typing.Mapping[KT, VT_co] instead of dict[KT, VT]. However, typing.Mapping is read-only. The mutable version typing.MutableMapping is invariant instead of covariant. Is there a type that is covariant and mutable?

No, that’s just not valid. Reread the documentation of co and contra variance.

If you think you have an example where it can would be somehow save to have a mutable non-invariant container type, please show it.

No, because this is a contradiction in terms. Covariance means that we could treat “container of derived” like a subtype of “container of base”. But mutability, for “container of base”, means that we can add a base instance to it - whether or not that’s also a derived instance. And subtyping, by the Liskov Substitution Principle, means that we can do anything with a subtype that we could do with the base type. To be both covariant and mutable, therefore, we’d have to be allowed to add non-derived elements to a container of derived elements (because a container of base elements lets us do that). But that contradicts the definition of a container.

(I have to look up which one is “covariance” and which one is “contravariance” every time - but after that the logic flows naturally.)

1 Like

No, because this is a contradiction in terms.

I don’t think so. I’m looking for covariance only in the VT, i.e., almost the same as typing.MutableMapping[KT, VT] but is instead [KT, VT_co], not the full mutable mapping. The KT and the Mapping part is not required to be covariant.

The use case I have in mind is a function f(…)->Map[KT, VT1] and be able to assign the return value to a Map[KT, VT2] variable m, where VT1 is a subtype of VT2 (hence covariance in VT is desired) and be able to mutate m[k]=some VT2 values for some k in KT, without having to introduce additional intermediate variables or wrap in dict(…) | {k: …} gymnastic just to get round the typing system.

def f() -> Map[str, int]: ...

x: Map[str, object] = f()
x['foo'] = object()

Explain to me how that is not unsafe? You inserted an object into a Map which was supposed to contain int, if someone else has a reference to the same map their code will break because they can’t rely on the map only containing int anymore.

The only reason why Mapping is covariant in VT is exactly because this cannot happen, i.e. you can’t downcast and then insert new values in an existing Mapping that may be shared with someone else.

You could define a mutable mapping that’s covariant in VT if the only mutations you are allowing are clear, popitem etc… but I don’t think that’s going to help you very much. (In simple terms any method where the VT appears in the parameter list, is forbidden with a covariant type parameter, the only exception to that rule is __init__)

1 Like