taway
taway

Reputation: 1359

Type annotation for classmethod returning instance

How should I annotate a @classmethod that returns an instance of cls? Here's a bad example:

class Foo(object):
    def __init__(self, bar: str):
        self.bar = bar

    @classmethod
    def with_stuff_appended(cls, bar: str) -> ???:
        return cls(bar + "stuff")

This returns a Foo but more accurately returns whichever subclass of Foo this is called on, so annotating with -> "Foo" wouldn't be good enough.

Upvotes: 118

Views: 44526

Answers (3)

TrakJohnson
TrakJohnson

Reputation: 2087

Python version >= 3.11: Starting with Python 3.11, you can now use typing.Self to avoid declaring a TypeVar. It can be used with @classmethod, as specified in PEP 673:

The Self type annotation is also useful for classmethods that return an instance of the class that they operate on. For example, from_config in the following snippet builds a Shape object from a given config [...]

from typing import Self

class Shape:
    @classmethod
    def from_config(cls, config: dict[str, float]) -> Self:
        return cls(config["scale"])

This correctly handles subclassing, as required by the question:

class Circle(Shape):
    pass

Circle.from_config(config)  # mypy says this is a Circle

Upvotes: 29

Ignacio Vergara Kausel
Ignacio Vergara Kausel

Reputation: 5986

Just for completeness, in Python 3.7 you can use the postponed evaluation of annotations as defined in PEP 563 by importing from __future__ import annotations at the beginning of the file.

Then for your code it'd look like

from __future__ import annotations

class Foo(object):
    def __init__(self, bar: str):
        self.bar = bar

    @classmethod
    def with_stuff_appended(cls, bar: str) -> Foo:
        return cls(bar + "stuff")

As per the docs, this import will effectively be automatic starting with Python 3.11.

Upvotes: 69

Michael0x2a
Michael0x2a

Reputation: 63978

The trick is to explicitly add an annotation to the cls parameter, in combination with TypeVar, for generics, and Type, to represent a class rather than the instance itself, like so:

from typing import TypeVar, Type

# Create a generic variable that can be 'Parent', or any subclass.
T = TypeVar('T', bound='Parent')

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

    @classmethod
    def with_stuff_appended(cls: Type[T], bar: str) -> T:
        # We annotate 'cls' with a typevar so that we can
        # type our return type more precisely
        return cls(bar + "stuff")

class Child(Parent):
    # If you're going to redefine __init__, make sure it
    # has a signature that's compatible with the Parent's __init__,
    # since mypy currently doesn't check for that.

    def child_only(self) -> int:
        return 3

# Mypy correctly infers that p is of type 'Parent',
# and c is of type 'Child'.
p = Parent.with_stuff_appended("10")
c = Child.with_stuff_appended("20")

# We can verify this ourself by using the special 'reveal_type'
# function. Be sure to delete these lines before running your
# code -- this function is something only mypy understands
# (it's meant to help with debugging your types).
reveal_type(p)  # Revealed type is 'test.Parent*'
reveal_type(c)  # Revealed type is 'test.Child*'

# So, these all typecheck
print(p.bar)
print(c.bar)
print(c.child_only())

Normally, you can leave cls (and self) unannotated, but if you need to refer to the specific subclass, you can add an explicit annotation. Note that this feature is still experimental and may be buggy in some cases. You may also need to use the latest version of mypy cloned from Github, rather then what's available on pypi -- I don't remember if that version supports this feature for classmethods.

Upvotes: 131

Related Questions