-
Notifications
You must be signed in to change notification settings - Fork 250
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
Optional class and protocol fields and methods #601
Comments
This was explicitly deferred in PEP 544. On the other hand this would be not hard to implement now. Also another structural type in mypy -- @JukkaL what do you think? |
I don't think a |
It is always sufficient. Whether it is convenient is a different question. |
I mentioned this again on typing-sig. It could reuse PEP 655's class MyProto:
x: NotRequired[int]
@not_required
def foo(self) -> None: ... It would also make sense for non-protocols: class Foo:
x: NotRequired[int]
def setup(self) -> None:
self.x = some_calc() (Not a design I would recommend, but I have seen this from time to time in the wild.) |
I think this could be helpful for solving #1498 by marking |
It's 2024 and I need this for a scenario where I want to type classes that may optionally implement a method. My function takes in instances that should match a protocol, where one method ( class MyClassProtocol(Protocol):
pass
# Can't put this in or that'd make it mandatory:
# def reset() -> void: ...
def my_func(i: MyClassProtocol):
if hasattr(i, "reset"):
i.reset() # fails typecheck now
# other logic |
@ottokruse, here's a solution that might work for you: Code sample in pyright playground from typing import Protocol, runtime_checkable
@runtime_checkable
class MyClassProtocol1(Protocol):
def required_method(self) -> None:
...
@runtime_checkable
class MyClassProtocol2(MyClassProtocol1, Protocol):
def reset(self) -> None:
...
type MyClassProtocol = MyClassProtocol1 | MyClassProtocol2
def my_func(i: MyClassProtocol):
if isinstance(i, MyClassProtocol2):
i.reset()
# Test with some concrete classes
class Foo:
def required_method(self) -> None:
...
class Bar(Foo):
def reset(self) -> None:
...
my_func(Foo())
my_func(Bar()) |
Thank you Eric! Also found your typeguard PEP now. Nice work |
I did some number crunching here: https://github.com/yangdanny97/type_coverage_py/blob/hasattr_getattr/package_report.json and it looks like 65% of the top 2000 packages on pypi have calls to IMO it's common enough in existing code to warrant a PEP for this.
I'd advocate for If this was protocol-only, I get the sense that maintainers will grumble about writing extra classes just to satisfy the typechecker, and I worry it would be like Callable protocols where it's kind of verbose and relatively harder to adopt. |
I came up with some proposed semantics for this - feedback would be greatly appreciated SemanticsThe Structural TypecheckingIf a protocol has a NotRequired attribute, structural typechecking will depend on finality:
The requirement that implementing classes must declare the attribute as NotRequired (instead of allowing it to be omitted) is motivated by this example from @JelleZijlstra
@runtime_checkableAttributes annotated with NotRequired in a runtime_checkable protocol will be skipped when checking an object at runtime. The current runtime checkable behavior only checks for the presence of an attribute with the right name without checking the type. OverridesSubclasses may only remove the FinalNotRequired will not be compatible with the Final qualifier in the same type annotation, since attributes with the latter are required to be initialized so NotRequired wouldn’t do anything. ReadOnlyPEP767 may introduce read-only attributes. Subclasses will be allowed to override ReadOnly attributes to remove
Pattern MatchingNot-required attributes are not allowed to be used with Assignment/DeletionThere will be no changes to assignment or deletion behavior at runtime. For the purposes of typechecking, assignment to NotRequired attributes will work the same as attributes annotated without the qualifier. Currently, despite all attributes being “required”, none of the major typecheckers prevent attributes from being deleted. This behavior will stay the same, and both regular and NotRequired attributes will be deletable. AccessThere will be no changes to access behavior at runtime. Typecheckers may error if the attribute is accessed without being narrowed (using a This would be similar to emitting errors for accessing non-required TypedDict keys, and narrowing would work the same way (Mypy and Pyre don’t support this kind of error/narrowing, but Pyright does). Given the lack of standardization of the equivalent typechecking behavior for TypedDicts, I think we probably want to make this behavior optional for now.
Uninitialized Attribute ChecksTypecheckers including Mypy, Pyright, and Pyre, can check for uninitialized attributes, though this is generally a best-effort check and opt-in/experimental in some cases. Typecheckers should not raise an error if the uninitialized attribute is annotated with NotRequired. Effect for UsersWhen implementing all the required proposed features as described above (but none of the optional ones), this is what changes. Compared to leaving the attribute unannotated:
Compared to annotating the attribute with an unqualified type:
Things the proposed semantics do NOT guarantee:
|
How does it work with dataclasses? I suspect that dataclasses should inspect the type and check if it's |
So to declare an optional function signature you would do this?: class Class1((Protocol):):
fn: NotRequired[Callable[[Class1, ...], ...]] Ugly because you have to type |
Interesting question. Could you give a motivating example of a non-required method? I think this would help us understand the use case and appropriate solutions. [Edit: snippet a comment that isn't relevant to the original question. I missed that Class1 was a Protocol originally.] |
Here's an example in typeshed: https://github.com/python/typeshed/blob/a51dd6d6d86bf1bdf87c22cbacfe2fda231418dc/stdlib/bz2.pyi#L17 File objects must have a |
Thanks! That's a great example. I took a peek at the bz2 implementation, and I am not sure that
Based on that reading, I'd say that |
I think you're right about this particular case, but there's a number of similar ones elsewhere. Grep for Another example I found was the |
I would like to add another example, because in typeshed a class with maximum number of optional method has only 4 optional method (usually 2). My example is a "control" class in FreeCAD. Depending on if the method is defined in a provided class, it will be called in C++.
Also, I would like to point to a missing ability in class DoubleClickedMethod(Protocol):
def __call__(self: ControlClass, viewObj: FreeCADGui.ViewProviderDocumentObject, /) -> bool: ...
class ControlClass(Protocol):
doubleClicked: NotRequired[DoubleClickedMethod] But annotating Could you @yangdanny97 explain your example why this is a problem?: accept_a(B()) # boom Or maybe there should be this code?: user(B()) # boom |
Regarding expressing possibly-present methods as The behavior of existing type checkers does't appear to be consistent here, and it appears to be unspecified:
That said I don't view standardizing/specifying this behavior as vital/blocking to the proposal, I think that could be done separately. |
Sorry, I messed up an indentation in the example. I've fixed it above, but the changed section is here: def accept_a(a: A) -> None:
user(a) # OK, A has an attribute a of the right type and doesn't have an attribute b
accept_a(B()) # boom
The proposed solution is to require A to declare the attribute as NotRequired in order to implement P, preventing incompatible overrides in subclasses class A:
a: int
b: NotRequired[str]
def __init__(self) -> None:
self.a = 3
class B(A):
b: int. # typecheck error |
Would be good to have datapoints, how often is this needed for attributes, how often for methods? Maybe simple enough to do in the number crunching script, that checks all the getattr calls, that you ran before? (which was super helpful) |
I can think of two options here, others can chime in if there are more ideas. The first/easiest option is just to ban this type qualifier in dataclasses. The second option is to have some special handling based on inspecting the type. @migeed-z suggested creating a new type |
So this is a bit tricky because the script I used only looks at the AST nodes, and it doesn't have the ability to look up the attr referenced in a I had an idea to adapt the script to search for The results from looking at the top 2k packages are as follows:
|
I discussed with @samwgoldman re: dataclasses If we do go with using a special uninitialized/undefined value for dataclasses as proposed above, we would probably want the values to be optional in the generated constructor, like so:
This would require placing all NotRequired fields at the end, similar to fields with default values. We might also want to make them keyword-only. |
The proposal for
Unfortunately that means that a significant class of problems – and especially those that motivated this issue – is not handled by this proposal. While some protocols could be described using the – awkward – |
With the semantics as currently proposed, allowing I don't think working around this is possible without runtime changes, but if we do make runtime changes it should be possible to support it for both protocols and classes. For example:
To make cc @JelleZijlstra @rchen152 for a sanity check on whether this would be a reasonable change to propose or if it would be too controversial In a type stub file, I think we could work around it by decorating the absent method with both
So I guess from what I can see, the options are:
if anyone else has any ideas please let me know :D |
I don't think that's realistic, sorry. It wouldn't be possible without changes to the language core, and I don't see a good way to make this work. It seems difficult to make this feature both sound and convenient to use. |
I cross-posted to the forum to get more feedback/ideas https://discuss.python.org/t/discussion-optional-class-and-protocol-fields-and-methods/79254 |
Sometimes, implementations check for the existence of a method on an object before using it. Take this example from Python 2.7's urllib:
Currently this is best modeled by leaving these methods out from a protocol. But this means that their signature is unknown, and mypy will complain about the non-existing attribute. It would be useful to be able to model this somehow, for example by using a decorator for such methods.
The text was updated successfully, but these errors were encountered: