Marcel Wilson
Marcel Wilson

Reputation: 4572

Type hinting additional attribute of a protocol?

Is it possible to type hint an additional attribute of an attribute of a class?

I have a class attribute Outer.inner that can be any instance of a class that conforms to a protocol InnerProto.

#### I do not control this code #######
from typing import Protocol

class InnerProto(Protocol):
    arg: int

class OuterProto(Protocol):
    inner: InnerProto

Inner class conforms to the protocol InnerProto, but it also has an additional attribute (which as far as I understand doesn't violate protocol conformity)

class Outer:
    def __init__(self, inner: InnerProto):
        self.inner: InnerProto = inner

class Inner:
    def __init__(self, arg: int):
        self.arg: int = arg  # this is part of the protocol
        self.arg2: int = arg  # this is not

Pycharm (v2021.2) inspection highlights the attribute arg2 as an unresolved attribute.

def test_type_hint_example():
    obj = Outer(Inner(1))
    assert obj.inner.arg == 1
    assert obj.inner.arg2 == 1 # inspection highlights arg2 as "unresolved attribute"

enter image description here

Is it possible to typehint so that inspection recognizes what arg2 is?

Normally I would be inclined to alter the type hint of Outer.inner but in this case that would be a mistake. inner can be ANY class that conforms to InnerProto.

My second thought was to override the type-hint for the attribute of the instance. This type of trick has worked non-attribute objects, but in this case it results in a different inspection error: "Non-self attribute could not be type hinted"

def test_option1():
    obj = Outer(Inner(1))
    obj.inner: Inner   # fixes inspection for arg2, but "Non-self attribute could not be type hinted" occurs
    assert obj.inner.arg == 1
    assert obj.inner.arg2 == 1

enter image description here

What other options are there?

Upvotes: 0

Views: 1281

Answers (1)

Alex Waygood
Alex Waygood

Reputation: 7539

I think the issue here is that the code isn't type-safe.

Your Outer.__init__ function is:

class Outer:
    def __init__(self, inner: InnerProto):
        self.inner = inner

    # <-- snip -->

You've annotated the inner argument with InnerProto. That means that the Outer.inner attribute could be of any type, as long as that type conforms to the InnerProto protocol.

According to you, this is the correct type-hint:

Normally I would be inclined to alter the type hint of Outer.inner but in this case that would be a mistake. inner can be ANY class that conforms to InnerProto.

If this is the case, then there is no way for a static type-checker to verify that Outer.inner will have any attributes or methods that are not specified in InnerProto. This is why the type-checker is raising an error in your test_type_hint_example function: you've told the type-checker that Outer.inner could be of any type as long as that type conforms to InnerProto, but InnerProto says nothing about an arg2 attribute, and not all python types have an arg2 attribute, so therefore there's a possibility that you'll get an AttribiteError when you access Outer.inner.arg2.

If you are certain that, in the context of this specific function, Outer.inner will be of type Inner, and therefore can be guaranteed to have an arg2 attribute — despite the fact that in most situations, Outer.inner "could be ANY class that conforms to InnerProto" — then you could use typing.cast like this:

from typing import cast

def test_type_hint():
    obj = Outer(Inner(1))
    inner = cast(Inner, obj.inner)
    assert inner.arg == 1
    assert inner.arg2 == 1

In reality, however, if you are certain that Outer.inner can be guaranteed to have an arg2 attribute, that most likely indicates that you do, in fact, have the wrong type hint for Outer.inner (it's hard to tell you what the right type hint is without seeing more of your code base). Using typing.cast in the way I've just demonstrated above should generally only be used as a last resort — it's more a method of evading the type-checker, which won't help you in the long run if your code isn't type-safe.

Upvotes: 1

Related Questions