-
Notifications
You must be signed in to change notification settings - Fork 2
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
Include a static type checker? #514
Comments
This is exclusively for static typing though, in which case we still need our existing type checking? Then for me including it vs recommending it is just a matter of how big the extra set of dependencies is. I didn't look at the whole tree, but the dependency list for the main package is non-trivial, so I'm neither closed to this nor immediately convinced. |
This is for our code base of |
For example: |
Not a great example -- this is a false positive. Check right above this in line 355. |
No, it is not. It is a static type checker. You will get runtime type checking by the python interpreter. The question is: Do you want the type hints to be a auto-completion helper for the user/IDE or a typing system? |
Ah, sorry, I had misunderstood the strictness of "static". It's not merely that it doesn't guarantee runtime behaviour, but it isn't parsing any of the transformation operations. Totally fair.
I mean, yes, of course, these are obviously pros. My only question is at what cost of cons? For instance, def replace_child(
self, owned_node: Node | str, replacement: Node | type[Node]
) -> Node:
if isinstance(owned_node, str):
owned_node = self.children[owned_node]
... # Proceed as though `owned_node` is a `Node` _in the scope of this function_ Is perfectly readable. Perhaps it's lack of imagination on my part, but if I am understanding correctly then to get the static linter happy with this would require something like def _make_sure_its_a_node(self, node: Node | str) -> Node:
if isinstance(node, str):
node = self.children[node]
return node
def replace_child(
self, owned_node: Node | str, replacement: Node | type[Node]
) -> Node:
self._replace_child(
self._make_sure_its_a_node(owned_node),
replacement
)
def _replace_child(
self, owned_node: Node, replacement: Node | type[Node]
) -> Node:
... And in general this is the pattern whenever we have type overloading for user convenience. I'm just not convinced that the pros of maintaining IDE hints inside the scope of the function is worth the con of extra misdirection. With that said, |
Fair enough, I would also not want to make the code essentially complexer by this. Does |
I just had a quick look at the documentation If I understand this correctly both of the following snippets should work fine: def replace_child(
self, owned_node: Node | str, replacement: Node | type[Node]
) -> Node:
if isinstance(owned_node, str):
owned_node = self.children[owned_node]
assert isinstance(owned_node, Node)
... # Proceed as though `owned_node` is a `Node` _in the scope of this function_ def replace_child(
self, owned_node: Node | str, replacement: Node | type[Node]
) -> Node:
if isinstance(owned_node, str):
owned_node_used: Node = self.children[owned_node]
else:
owned_node_used: Node = owned_node
... # Proceed as though `owned_node_used` is a `Node` _in the scope of this function_ |
Nice! I find the second one a little clunky, but the assert version is pretty slick |
Frankly speaking, I am not too familiar with Generally, I think everything not enforced and automatically checked is not existent. It might work for the first year and first few thousand lines of code, but in the long run it will get unreliable. It is a decision of general principle. Do we want to put the effort in and have the advantage of a strongly typed language, or are we ok with hints that will be correct most of the time but might also be incorrect. Since my background is in strongly typed languages I favour this approach. :) |
I should have spent more time in preparing the example. In fact, from typing import List
def func(a: List[int]|int):
if isinstance(a, int):
a = [a]
a.append(5) |
This is a type violation. if isinstance(owned_node, str):
owned_node = self.children[owned_node] returns The real problem is: I think this is a very good example illustrating my previous point of hints being only hints that cannot be trusted if they are not checked and enforced. |
Indeed, that's a typo and
But then, I think the solution from @niklassiemer with
I totally agree that if it's not in the CI it's not reliably there. But honestly it's not reliably there to begin with -- a decent amount of stuff has no type hint to start with. In some places, like adding an @property
def Composite.children(self) -> bidict[str, Node]:
return super().children And honestly I'm just not sure I want to commit to imposing a bunch of boiler plate on the python code. I definitely see the appeal and benefit of having the types be testably reliable, but, oof, I have flat zero desire to go around writing code like the snippet immediately above. Also, like you, I have very little (none in my case) experience with |
Letting it percolate more, and with the examples from @niklassiemer suggesting to us that @XzzX, in addition to ruff, would you be interested in getting a |
I did a first test run of
I think it is an helpful addition and a great step to improve our code quality. Still it might take some time to update all repositories to have strict type hints - currently the other packages like |
Well, you cannot blame
In my opinion the correct solution would be to make the code strongly typed (assuming we want it to be like that). If the current class hierarchy is to be preserved, it means making Of course, this introduces more complexity and burden for those not familiar with strongly typed languages. |
Another remark about unnotated stuff: def foo(a: int, b) -> int:
return a+b Is correct. If no type is specified, no check will happen. If you use the result of |
This is wrong and not an option. Consider the following: class A(Semantic): #<- this is not a Node
pass
def add(container: SemanticParent):
a = A(...)
container.children.append(a) #<- valid according to type definitions
foo: Composite = ...
add(foo) #<- incorrect, due to redefition of children The redefinition of |
This is a good benchmark. If the typical developer immediately sees the problem pointed out we can enforce strong typing. If not, and the typical developer thinks this is rather strange and complicated, we should not do it. |
Yes, this is exactly how the runtime checking works too -- if one side of a connection has no type hint at all, the type checking is simply skipped. |
Yes, this is a conclusive example that my first idea for how to handle the type narrowing is wrong.
TBH I have never used the generics or type vars. I played around with them a little bit this afternoon and see how to make the ...
from typing import Generic, TypeVar, Dict
...
T = TypeVar('T', bound='Semantic')
class SemanticParent(Generic[T]):
_children: bidict[str, T]
...
class Composite(..., SemanticParent[Node], ...):
... I'm not in love with it, but it's relatively compact and clear. Thanks for the pointer to
Realistically, this is not a trivial point. I'm not "blaming" I absolutely see the merit of having the type hints be provably correct. I hope the typing here is overall not too bad, but there are 100% going to be more type narrowing problems than you uncovered in this example, and it would be great to resolve them. It is not super high on my priority list, but I fully agree it is a positive change. The catch is that while it makes the code base easier to learn and more reliable (a clear net win), it only does so for people who are already familiar with these tools/ideas. It requires this extra knowledge, which presents a barrier to getting other developers who don't already have this knowledge to maintain and expand the project at all. So I see some intrinsic tension between making things easier for a hypothetical "ideal" dev, and our actual real team. At present, I would be surprised if anyone on the team saw So I like the idea, and I'd like to move forward with it, but I don't think it's a good idea without at least moderate buy-in from the rest of the team. Are others willing to learn about generics, type vars, and willing to spend the time making sure stuff passes Getting people to use |
Since
pyiron_workflow
uses type hints we should include a static type checker like (https://mypy.readthedocs.io/en/stable/index.html)[mypy] in the CI.The text was updated successfully, but these errors were encountered: