Required/NotRequired and inheritance #1516
Replies: 5 comments 6 replies
-
I'm not convinced these should be errors. Protocols are also used to define structural types, but a protocol can define inheritable default implementations. A At best, this is an error that's telling the user they may be doing something that they don't intend. That may be appropriate for a linter, but I'm not convinced it's something a type checker should be flagging as an error. I'll note that pyright correctly reports that a value of type def func(a: A, b: B):
a = b # Type violation
b = a # Type violation Pyright is arguably inconsistent in that it emits an error when overriding a class A(TypedDict):
a: int
b: int | str
c: int
d: ReadOnly[int | str]
class B(A):
a: str # Pyright emits error
b: str # Pyright emits error
c: int # No error
d: str # No error Mypy seems to be overly restrictive in that it doesn't allow overriding a class A(TypedDict):
a: int
class B(A):
a: int # Mypy emits error here even though `a` is declared with exactly the same type
b: int Here's another interesting datapoint. TypeScript, which uses structural typing exclusive, does complain if you attempt to override a field with an incompatible type. However, it treats fields as covariant, even when they're mutable. interface A1 { a: number }
interface B1 extends A1 { a: string } // Error
interface A2 { a: number | string }
interface B2 extends A2 { a: string } // No error TypeScript doesn't complain if you attempt to redefine a "not required" field as "required", but it does complain in the opposite direction. export interface A3 { a?: number }
export interface B3 extends A3 { a: number } // No error
export interface A4 { a: number }
export interface B4 extends A4 { a?: number } // Error And TypeScript doesn't complain if you attempt to redefine a "readonly" field as writable or vice versa. export interface A5 { readonly a: number }
export interface B5 extends A5 { a: number } // No error
export interface A6 { a: number }
export interface B6 extends A6 { readonly a: number } // No error It looks like TypeScript emits errors only when the resulting type would no longer be type compatible with the type it extends. This makes sense, especially for developers who are used to class hierarchies and nominal typing rules. It also gives the type checker the option of using a performance shortcut when determining type compatibility. I can see three defensible (type-safe and internally consistent) options:
Of these, I think option 2 strikes the right balance. This is what TypeScript does. It's also consistent with what draft PEP 705 proposes for the If we go with option 2, then pyright and mypy would both need to make minor changes. |
Beta Was this translation helpful? Give feedback.
-
PEP-705 was written under the assumption that a subclass must be structurally compatible with all of its superclasses. That is: class A(TypedDict):
foo: Sequence[int]
bar: ReadOnly[Sequence[int]]
class B(A):
foo: list[int] # type error
class C(A):
bar: list[int] # not a type error I think this is option 3 in your list (assuming I understand 2 correctly from "this is what Typescript does"). It would avoid this situation: b: B = { ... }
a: A = b # Type error, even though B inherited from A! |
Beta Was this translation helpful? Give feedback.
-
I don't think it can be option 2 if option 2 is supposed to include the examples you give of legal TypeScript. For instance, you gave export interface A6 { a: number }
export interface B6 extends A6 { readonly a: number } // No error This would map to class A6(TypedDict):
a: int | float
class B6(A6):
a: ReadOnly[int | float] I was fully intending for this to be rejected by type-checkers, because otherwise we get this: class C6(B6):
a: int
c: C6 = { "a": 5 }
b: B6 = c
a: A6 = b # type error: 'a' is read-only in B6 but not in A6
a['a'] = 3.4 # now c is not a valid C6 So perhaps we have four options:
where 3 and 4 are identical if you do not use PEP-705/ReadOnly. |
Beta Was this translation helpful? Give feedback.
-
I've quickly double-checked the wording of PEP-589 and PEP-705 and as far as I can tell they collectively rule out all but option 3.
The TypeScript example I'm using to conclude PEP-589 rules out 2 is: interface A2 { a: number | string }
interface B2 extends A2 { a: string } // No error That said:
As long as all type-checkers agree on what the actual meaning is for anything they mutually admit as legal, I like Eric's statement:
|
Beta Was this translation helpful? Give feedback.
-
Do we want this to apply to Protocols too? I have come across the following interesting case: class A(Protocol):
x: Final[int | None]
class B(Protocol):
x: Final[int]
def b_to_a(b: B) -> A:
return b
class C(A, Protocol):
x: Final[int] Passing this into mypy, I get:
I'm not sure why mypy is preventing use of Final on Protocols. (Maybe to avoid this whole debate!) On the other hand, pyright gives me just:
But no error on the line Is this because it could provide a value, and pyright doesn't track that, so has to assume it does? Or is this just stemming from a literal reading of "prevents redefinition in subclasses" in the PEP? (This came up because someone asked for clarification on why I didn't reuse |
Beta Was this translation helpful? Give feedback.
-
Consider this code:
Mypy says:
Pyright produces no errors.
(And for good measure, the CPython runtime also has a bug: python/cpython#112509.)
I think mypy is right here and both cases should be rejected:
x
of typeA
, it's legal to dodel a["a"]
, but ifx
is of typeB
, that's not allowed.A
must have a key"b"
, so a type that doesn't require"b"
cannot be a subtype ofA
.This logic would be a bit different if PEP-705 is accepted.
Beta Was this translation helpful? Give feedback.
All reactions