0

I have the following situation: a parent class Format and some child classes, for example FormatA and FormatB:

import abc

class Format(abc.ABC):

    def to_formatA(self):
        raise NotImplementedError()

    def to_formatB(self):
        raise NotImplementedError()
    ...


class FormatA(Format):
   
    def to_formatA(self):
        return self
    
    def to_formatB(self):
        return FormatB()


class FormatB(Format):
    def to_formatA(self):
        return FormatA()
    
    def to_formatB(self):
        return self

What is the best way to apply typing annotations?

My Attempt:


import abc
from typing import TypeVar

TFormat = TypeVar("TFormat", bound="Format")
TFormatA = TypeVar("TFormatA", bound="FormatA")
TFormatB = TypeVar("TFormatB", bound="FormatB")


class Format(abc.ABC):

    def to_formatA(self) -> TFormatA:
        raise NotImplementedError()

    def to_formatB(self) -> TFormatB:
        raise NotImplementedError()


class FormatA(Format):

    def to_formatA(self) -> TFormatA:
        return self

    def to_formatB(self) -> TFormatB:
        return FormatB()


class FormatB(Format):

    def to_formatA(self) -> TFormatA:
        return FormatA()

    def to_formatB(self) -> TFormatB:
        return self

MyPy gives me different errors:

  1. for each class, to_formatA and to_formatB methods give:
error: A function returning TypeVar should receive at least one argument containing the same TypeVar

Type Hint for self?

  1. for row 21 and 33, respectively:
error: Incompatible return value type (got "FormatA", expected "TFormatA")
error: Incompatible return value type (got "FormatB", expected "TFormatB")
Papemax89
  • 9
  • 2

2 Answers2

2

You can use a string with the class name as a forward declaration.

import abc

class Format(abc.ABC):

    def to_formatA(self) -> "FormatA":
        raise NotImplementedError()

    def to_formatB(self) -> "FormatB":
        raise NotImplementedError()
    ...


class FormatA(Format):
   
    def to_formatA(self) -> "FormatA":
        return self
    
    def to_formatB(self) -> "FormatB":
        return FormatB()


class FormatB(Format):
    def to_formatA(self) -> "FormatA":
        return FormatA()
    
    def to_formatB(self) -> "FormatB":
        return self
$ mypy a.py
Success: no issues found in 1 source file

Alternatively, you can use from __future__ import annotations and use the annotations as-is. They'll be ignored by Python, but processed correctly by mypy:

from __future__ import annotations
import abc

class Format(abc.ABC):

    def to_formatA(self) -> FormatA:
        raise NotImplementedError()

    def to_formatB(self) -> FormatB:
        raise NotImplementedError()
    ...


class FormatA(Format):
   
    def to_formatA(self) -> FormatA:
        return self
    
    def to_formatB(self) -> FormatB:
        return FormatB()


class FormatB(Format):
    def to_formatA(self) -> FormatA:
        return FormatA()
    
    def to_formatB(self) -> FormatB:
        return self
$ mypy a.py
Success: no issues found in 1 source file
BoppreH
  • 8,014
  • 4
  • 34
  • 71
  • Thank you @BoppreH. However, I thought it was not generally the best choice to use strings and forward declaration. Am I Wrong? There is no way to handle the above situation with `TypeVar` and more in general without *forward declaration*? – Papemax89 Jul 08 '23 at 19:19
  • 2
    @Papemax89 I don't think it's frowned upon. It's certainly simpler, and mypy doesn't seem to care either way. For more info, [this other question](https://stackoverflow.com/questions/33533148/how-do-i-type-hint-a-method-with-the-type-of-the-enclosing-class). – BoppreH Jul 08 '23 at 19:25
  • This seems entirely reasonable. But since it is generally a good idea to make return type annotations as _narrow_ as possible, I would suggest using [`typing.Self`](https://docs.python.org/3/library/typing.html#typing.Self) to annotate `FormatA.to_formatA` and `FormatB.to_formatB`. This is will implicitly further constrain the return type, in case either for those classes is subclassed again. (For Python `<3.11` you can import it from `typing_extensions`.) – Daniil Fajnberg Jul 10 '23 at 12:06
0

Sorry can't add a comment as I don't have enough reputation, but simply adding from __future__ import annotations should work:

from __future__ import annotations

import abc


class Format(abc.ABC):

    def to_formatA(self) -> FormatA:
        raise NotImplementedError()

    def to_formatB(self) -> FormatB:
        raise NotImplementedError()
    ...


class FormatA(Format):
   
    def to_formatA(self) -> FormatA:
        return self
    
    def to_formatB(self) -> FormatB:
        return FormatB()


class FormatB(Format):
    def to_formatA(self) -> FormatA:
        return FormatA()
    
    def to_formatB(self) -> FormatB:
        return self

It remedies errors for me in VSCode, I am not familiar with MyPy however.

Nater0214
  • 3
  • 4
  • Thanks @Nater0214 for your response. However your proposal doesn't resolve mypy errors. – Papemax89 Jul 08 '23 at 19:26
  • FYI, this *is* an answer, so you don't need to apologize for posting it as such instead of as a comment. (It's too often assumed that comments are appropriate for "trivial" answers.) – chepner Jul 13 '23 at 13:27
  • (I won't claim I never do it, but I *strive* to post a comment-answer only when using it to clarify a close-as-duplicate or close-as-typo vote.) – chepner Jul 13 '23 at 13:28
  • Thanks for the info @chepner. I'll keep that in mind. – Nater0214 Jul 18 '23 at 00:33