0

That title is a handful, so let me share a code snippet first...

class Parent:
    class GenericNode():
        def __init__(self, name: str) -> None:
            self.name: str = name

    class SpecificNode(Node):
        def __init__(self, name: str) -> None:
            super().__init__(name)
            self.node_list: list[Parent.GenericNode] = []

        def add_node(self, node: Parent.GenericNode) -> None:
            self.node_list.append(node)

Here, in the add_node(self, node: Parent.GenericNode) method, the Parent.GenericNode type is not recognised by the Python interpreter - it throws a NameError when trying to run it. That seems very odd to me, since the exact same type was just used to define the type of the list in the constructor.

However, when I take everything one level up - and get rid of the Parent class altogether like this:

class Node():
    def __init__(self, name: str) -> None:
        self.name: str = name

class SpecificNode(Node):
    def __init__(self, name: str) -> None:
        super().__init__(name)
        self.node_list: list[Node] = []

    def add_node(self, node: Node) -> None:
        self.node_list.append(node)

... everything miraculously works, and the code executes.

I came across this answer, which suggests using a string in place of a type hint like so: add_node(self, node: 'Parent.GenericNode'). Now this code executes and even mypy manages to sucessfully type-check it.

However, as that answer also mentions, this (or rather the equivalent of using from __future__ import annotations) should have been the default since Python 3.10 and I'm testing this on Python 3.11. Was it perhaps not made part of the release?

I tried searching online but couldn't find any mention of this particular behaviour. Could someone please help me shed some light on this?

EDIT: I should also mention, I find it weird that there would be a problem with the Parent being undefined like many other answers suggest (e.g.), because that very type annotation is used in inside another method on the same level. The oddity to me is that in parameter type hint it does not work, but in a member variable type hint, it does.

Marty Cagas
  • 350
  • 5
  • 14
  • The future of `annotations` becoming mandatory is somewhat up in the air at this point. – chepner Jan 24 '23 at 23:01
  • 4
    `from __future__ import annotations` indeed was delayed (no idea when it's supposed to happen now, maybe 3.12?). In any case, use a string or use `from __future__ import annotations`. Or perhaps even better, **don't use a nested class**. It is not a common pattern in Python and doesn't do anything useful except making everything unwieldy, as you are learning. – juanpa.arrivillaga Jan 24 '23 at 23:01
  • 2
    As for why it helps, the name `Parent` has not yet been bound to the class under construction. – chepner Jan 24 '23 at 23:02
  • 3
    "everything miraculously works" it's not miraculous. The reason `Parent.GenericNode` doesn't work is because `Parent` **doesn't exist when at the point the code is executed**. Note, `GenericNode` *does* exist, and would work, not sure how static analysis tools like `mypy` will handle it. Again, the best thing to do is simply not to nest a class definition. – juanpa.arrivillaga Jan 24 '23 at 23:03
  • 1
    So, it's the same issue as doing `class Foo: ...` and then maybe `bar = 42` in the class block, then on the next line, try to do `print(Foo.bar + 1)` and you'll get a similar error message complaining that `Foo` is undefined (because it is) – juanpa.arrivillaga Jan 24 '23 at 23:04
  • 2
    "The oddity to me is that in parameter type hint it does not work, but in a member variable type hint, it does." It's not odd because your `self.node_list: list[Parent.GenericNode]` doesn't run *until the method is called* at which point, the global name has been bound. They key thing to understand is that the code in the class body is executed during the class definition statement, but the code in a function body isn't executed until the function *is called* – juanpa.arrivillaga Jan 24 '23 at 23:05
  • Actually, `GenericNode` doesn't exist, so that wouldn't work either, because class bodies don't create enclosing scopes. See, just another small trap you can fall in why you try to use nested classes. – juanpa.arrivillaga Jan 24 '23 at 23:11
  • 2
    (Also function-local annotations aren't evaluated at all, ever, even when executing the function, so you could have annotated `self.node_list` as `1/0` without throwing an error. mypy would say "hey that's not valid", but Python wouldn't care.) – user2357112 Jan 24 '23 at 23:16
  • @juanpa.arrivillaga Thank you for the inputs! I see, I was really missing some key concepts here. If you were to write this into an answer, I'd gladly accept it. As for why I'm doing all of that in my code... I'm trying to learn a couple new concepts that I never did in Python at once, but instead of helping, things like `mypy` just confused me even more this time... – Marty Cagas Jan 24 '23 at 23:17
  • @user2357112 According to [PEP 526](https://peps.python.org/pep-0526/#class-and-instance-variable-annotations), _"... instance variables can be annotated in `__init__` or other methods, rather than in the class."_ - that's why I tried doing it in the first place. I believe this shouldn't fall under function-local annotations, or am I incorrect? – Marty Cagas Jan 24 '23 at 23:21
  • 1
    Annotations defined within a function body are never evaluated, no matter what the annotation target is. (Annotations for the arguments and return value are evaluated, but not annotations within the body.) – user2357112 Jan 24 '23 at 23:22

0 Answers0