-
Notifications
You must be signed in to change notification settings - Fork 258
How should subtyping work with respect to properties? #1364
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
This is working as designed. If a function is annotated to return You asserted that "This snippet will fail at runtime". I'm not sure what you mean by "fail". The code raises an exception, but that appears to be the intent of the code, so I don't consider it a failure. As you know, static type checkers don't verify exception logic or enforce exception constraints. |
Indeed, but there are also no return statements at all. Mypy will emit an class Foo:
def x(self) -> int: ... I'm curious about why this should be considered any different, as there is no return statement here either; yet mypy emits no error: class Foo:
@property
def x(self) -> int:
raise AttributeError (It doesn't appear as though pyright has an equivalent check to mypy's
Good to know, thank you! I have little experience with other languages, which was part of the reason why I was curious if others had any insight here that I was missing, and why I posted this issue.
The I would argue it would be useful for a type checker to complain about missing return statements in |
My main issue from pure static typing view (ignoring runtime implications of checking) is inconsistency with NoReturn/normal methods. NoReturn roughly means either every code path is an exception or it runs forever. NoReturn is compatible with every time. When you have a function that’s callable[…, X] for any type X you can always use NoReturn. Languages that are much stricter about type systems similarly are happy to allow you to have a function that always errors have signature like return int. This also treats properties as a special, but same effect happens with normal methods. class HasBark(Protocol):
def bark(self) -> str:
…
class A:
def bark(self) -> str:
return NotImplementedError(“fail”)
X: HasBark = A type checks fine for both pyright and mypy. I’d consider not implemented error closest equivalent for methods to an attribute error for attributes. |
I agree with that -- though in my opinion, unconditionally raising |
Should it be allowed (no type error) if method was marked abstract? In a more realistic example it’s likely the code would instead be closer, def use_bark(obj: HasBark) -> None:
obj.bark()
…
def foo(obj: A):
use_a(obj) # Relies on A instance matching HasBark. Here it’s common usage for actual object passed to be subtype that would implement method. This partly is similar to how type[T] and abstract types are treated in mypy and pyright differently by default. Pyright behavior of allow abstract types to match type[T] is preferable to me as I do write a fair bit of code that doesn’t care if type is directly instantiable. Other aspect is these error types don’t show up in type signature at all so would be a property of function you can never see from a stub. I’m not sure these rules can be described in .pyi files besides just leaving method/attribute out which feels more confusing. edit: Fixed code formatting. |
I think mypy got that one wrong. Pyright interprets I think you're proposing that if a type checker can prove that exceptions are raised on all code paths, it should report a type violation if the return type annotation is not
A property is a function, and developers can implement this function any way they see fit. If they decide to explicitly raise an exception along some (or even all) code paths, that's a valid implementation of a property. The type checker has done its job by verifying that such a property exists and that all returned values will conform to the specified return type. If you want to enforce the convention in your code base that property implementations are not allowed to raise exceptions on some or all code paths, that sounds more like a linter rule, not the concern of a type checker. |
I'm a fan of mypy's behaviour here; I think we'll just have to agree to disagree on this point.
Yes, that is correct.
Understood -- and, to be clear, I don't consider it a bug that type checkers don't currently emit an error on the code in my initial post. More of a "missing feature" -- but one that I at least would find quite valuable. |
In my opinion, yes, because as soon as the property/method has been marked as abstract, the type checker will start disallowing dangerous calls. Here's mypy's behaviour if we add from abc import abstractmethod
from typing import Protocol
class HasX(Protocol):
@property
@abstractmethod
def x(self) -> int: ...
class Nominal(HasX):
@property
@abstractmethod
def x(self) -> int:
raise AttributeError
class Structural:
@property
@abstractmethod
def x(self) -> int:
raise AttributeError
def needs_obj_with_x(obj: HasX) -> None:
print(obj.x)
obj1 = Nominal() # error: Cannot instantiate abstract class "Nominal" with abstract attribute "x" [abstract]
obj2 = Structural() # error: Cannot instantiate abstract class "Structural" with abstract attribute "x" [abstract]
needs_obj_with_x(obj1)
needs_obj_with_x(obj2) |
I don't think that |
I agree with this. The proposal in the OP would effectively require the use of the |
Mypy's abstract error produces a lot of false positives in code that does more runtime introspection. from typing import TypeVar
from abc import ABC, abstractmethod
T = TypeVar("T")
def print_type(x: type[T]) -> None:
print(x.__name__)
class Foo(ABC):
@abstractmethod
def bar(self):
...
print_type(Foo) is entirely safe at runtime, but mypy flags it with an error I think I've covered my view in full. I consider abstract/notimplemented/attributeerror to be separate from whether method/attribute is defined. Errors are fully valid implementations and depending on usage may even be safe. Using errors to mean undefined also interact awkwardly with subtyping relations. If other type checkers were to introduce rule like this, I'd prefer the rule be separate error code to disable similar to how mypy rule had a long issue debate over abstract vs concrete type usage. |
It sounds like the consensus is pretty clearly against me here. Thanks everybody for giving their opinions, much appreciated! |
Consider the following snippet:
This snippet will fail at runtime, as accessing the
x
attribute on either an instance ofNominal
or an instance ofStructural
will result inAttributeError
being raised. However, neither mypy nor pyright sees any issue with this code, which surprises me. I would have expected a type checker to complain about theNominal.x
property and theStructural.x
property, since they are both annotated as returningint
yet neither of them will ever return anint
. I would have expected a type checker to demand that thex
property on bothNominal
andStructural
should either have at least one return statement (that returns anint
), or should be explicitly decorated with@abstractmethod
.Is this a missing feature from mypy and pyright? Or is there a reason for this not to be implemented?
(I haven't checked the behaviour of other type checkers.)
The text was updated successfully, but these errors were encountered: