Forward reference to enclosing class in optional return type, without typing.Optional?

Python 3.10 introduced the Foo | None alternative type annotation syntax for Optional[Foo]. This doesn’t seem to work when using forward reference to the enclosing type, like 'Foo' | None, which instead results in a runtime error:

class Foo:
    @staticmethod
    def maybe_create(arg: str) -> 'Foo' | None:
        if len(arg) < 5:
            return Foo()
        else:
            return None

if __name__ == '__main__':
    import sys
    print(Foo.maybe_create(sys.argv[1]))
Traceback (most recent call last):
  File "staticmethod-test.py", line 1, in <module>
    class Foo:
  File "staticmethod-test.py", line 3, in Foo
    def maybe_create(arg: str) -> 'Foo' | None:
                                  ~~~~~~^~~~~~
TypeError: unsupported operand type(s) for |: 'str' and 'NoneType'

Using typing.Optional works as expected, like:

from typing import Optional

class Foo:
    @staticmethod
    def maybe_create(arg: str) -> Optional['Foo']:
        if len(arg) < 5:
            return Foo()
        else:
            return None

if __name__ == '__main__':
    import sys
    print(Foo.maybe_create(sys.argv[1]))

Is there a way to express this optionality without extra imports?

>Solution :

Yes. If the union type contains forward references you have to wrap the entire return type in quotes.
Quoting from the documentation,

The | operand cannot be used at runtime to define unions where one or
more members is a forward reference. For example, int | "Foo", where
"Foo" is a reference to a class not yet defined, will fail at runtime.
For unions which include forward references, present the whole
expression as a string, e.g. "int | Foo"
.

class Foo:
    @staticmethod
    def maybe_create(arg: str) -> "Foo | None":
        if len(arg) < 5:
            return Foo()
        else:
            return None

Leave a Reply