I can't find any question that covers this closely, so will try to explain.
What does "same type" mean for type checking
Well, probably this is the long form of "you can't, and you shouldn't".
Type checking aims to confirm that all functions are called with proper argument types and return expected types. I suggest to read PEP483 first to understand concept of type better. Suppose you have the following:
s1 = ''.join(['a', 'b', 'c'])
s2 = ''.join(['a', 'b', 'c'])
assert s1 is not s2
assert s1 == s2
(join to avoid optimization, but it's another story). Are they the same object? No, is not clearly states this (they have different memory addresses). But will s2 be acceptable whenever you want s1? Definitely yes. You will not create a function that operates only on s1 and checks this fact with is, right?
Now what is the difference between self as reference to exact object and self as any A instance? When we talk about type checking, all A instances are completely equivalent and indistinguishable. They have the same set of methods and attributes (including types). We can ask: "which type errors can be introduced or removed, if we explicitly declare object to be self instance and not just self type?" I really cannot think of any. If you want this for semantics, use docstring - types should not be abused for everything. self object is absolutely the same as any other A() instance for type checker.
Simple solution: return class instance
Your first code sample is almost fine. Annotate return as A to tell that it returns instance of class A, it will work for final class:
class A:
def foo(self) -> A:
return self
However, this approach has a drawback (it is good explained in PEP673 about Self type):
class AChild(A):
pass
# mypy
reveal_type(AChild().foo()) # N: revealed type is "__main__.A"
If you create new A in foo and return it, then this approach is perfect. If you return self - it is valid, but not precise. That's why we need Self type.
Self type is too modern
As of now, mypy does not support Self type described in PEP673, so it fails with a cryptic message. However, your usage in 3rd example was perfectly valid and will work after implementation in type checkers (see 5-th code block in "Motivation" section of PEP).
Here's how you can use Self (assuming python=3.11 not to bother with typing_extensions and near future for mypy support):
from typing import Self
class A:
def foo(self) -> Self:
return self
class AChild(A):
pass
# mypy
reveal_type(AChild().foo()) # N: revealed type is "__main__.AChild"
reveal_type(A().foo()) # N: revealed type is "__main__.A"
It is great, but does not work now. Note that other checkers (namely pyright and pyre) have support for this.
Implementing Self type without using it
However, you can mimic Self accurately with a few lines of code (and python >= 3.7, AFAIR).
from typing import TypeVar
_Self = TypeVar('_Self', bound='A')
class A:
def foo(self: _Self) -> _Self:
return self
class AChild(A):
pass
# mypy
reveal_type(AChild().foo()) # N: revealed type is "__main__.AChild"
reveal_type(A().foo()) # N: revealed type is "__main__.A"
Now this works. All subclasses will return their class instance.