0

In the following code, I have two classes that contain methods that are supposed to return instances of the other, and without annotations works as expected:

class Foo:
    def __init__(self, bar = None):
        self.bar = bar or Bar(self)
    def foo(self):
        return self.bar

class Bar:
    def __init__(self, foo = None):
        self.foo = foo or Foo(self)
    def bar(self):
        return self.foo

f = Foo() # verify everything works as intended
assert f.foo().bar() == f 

However, adding type annotations to the code results in a NameError for any annotations on Foo indicating Bar as the type, since Bar is not yet declared:

from typing import *
class Foo:
    def __init__(self, bar : Optional[Bar] = None):
        self.bar = bar or Bar(self)
    def foo(self) -> Bar:
        return self.bar

class Bar:
    def __init__(self, foo : Optional[Foo] = None):
        self.foo = foo or Foo(self)
    def bar(self) -> Foo:
        return self.foo

In this case simply switching the order doesn't help either, since Bar also needs to reference Foo in its annotations.

How can I annotate functions with class that is not yet declared?


In other languages this would be handled with a forward declaration, however I'm not aware of such a feature in python. I've tried the "obvious" way to simulate a forward declaration (declaring an empty class), and while it appears to work on the surface, actually inspecting the annotations reveals that this approach is flawed:

from typing import *
class Bar:
    pass

class Foo:
    def __init__(self, bar : Optional[Bar] = None):
        self.bar = Bar(self) if bar is None else bar
    def foo(self) -> Bar:
        return self.bar

class Bar:
    def __init__(self, foo : Optional[Foo] = None):
        self.foo = Foo(self) if foo is None else foo
    def bar(self) -> Foo:
        return self.foo

assert get_type_hints(Bar.bar)['return'] == Foo #correct

# but the original `Bar` used in the annotation is different
# from the redeclared one so this fails:
assert get_type_hints(Foo.foo)['return'] == Bar 

Another possibility would be to annotate Foo with a base class of Bar, but this starts getting fairly messy:

from typing import *
from abc import ABCMeta, abstractmethod
class Bar_Base(object, metaclass=ABCMeta):
    @abstractmethod
    def __init__(self):
        pass

class Foo:
    def __init__(self, bar : Optional[Bar_Base] = None):
        self.bar = Bar(self) if bar is None else bar
    def foo(self) -> Bar_Base:
        return self.bar

class Bar(Bar_Base):
    def __init__(self, foo : Optional[Foo] = None):
        super().__init__()
        self.foo = Foo(self) if foo is None else foo
    def bar(self) -> Foo:
        return self.foo

assert issubclass(Foo, get_type_hints(Bar.bar)['return']) #correct
assert issubclass(Bar, get_type_hints(Foo.foo)['return']) #correct

How can I annotate functions with class that is not yet declared?

AJMansfield
  • 4,039
  • 3
  • 29
  • 50
  • The most intuitive solution is to reorder the code. – Prometheus Sep 19 '18 at 15:24
  • @SVengat That's a bit difficult when you have a circular relationship. – deceze Sep 19 '18 at 15:24
  • If you want to see how Python files are being executed, you see that they go line-by-line an execute along the way. Therefore, while at the lines of code describing Foo, you probably wouldn't have visited Bar yet. Therefore, as far as I know, I do not think it is possible. A reason why it works in other languages is because compilation allows the code to "forecast" the existence of an upcoming class. – Prometheus Sep 19 '18 at 15:26
  • See here: https://stackoverflow.com/questions/46605651/variable-annotations-on-a-class – Brad Campbell Sep 19 '18 at 15:28
  • 1
    @SVengat The short answer from the dupe: you can, simply use a string as "forward" declaration – Patrick Artner Sep 19 '18 at 15:28

0 Answers0