1

Given the following display function,

def display(some_object):
    print(some_object.value)

is there a way to programmatically determine that the attributes of some_object must include value?

Modern IDEs (like PyCharm) yield a syntax error if I try to pass an int to the display function, so they are obviously doing this kind of analysis behind the scenes. I am aware how to get the function signature, this question is only about how to get the (duck) type information, i.e. which attributes are expected for each function argument.

EDIT: In my specific use case, I have access to the source code (non obfuscated), but I am not in control of adding the type hints as the functions are user defined.

Toy example

For the simple display function, the following inspection code would do,

class DuckTypeInspector:
    def __init__(self):
        self.attrs = []

    def __getattr__(self, attr):
        return self.attrs.append(attr)

dti = DuckTypeInspector()
display(dti)
print(dti.attrs)

which outputs

None  # from the print in display
['value']  # from the last print statement, this is what i am after

However, as the DuckTypeInspector always returns None, this approach won't work in general. A simple add function for example,

def add(a, b):
    return a + b

dti1 = DuckTypeInspector()
dti2 = DuckTypeInspector()
add(dti1, dti2)

would yield the following error,

TypeError: unsupported operand type(s) for +: 'DuckTypeInspector' and 'DuckTypeInspector'
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
emher
  • 5,634
  • 1
  • 21
  • 32
  • 1
    There's no way to do this in general because it's easy to obfuscate access. That's why voluntary type hinting is the sane way to do this – Mad Physicist Jul 05 '21 at 15:43
  • Could you elaborate on how IDEs do it, @MadPhysicist? PyCharm seems to be right more-or-less all the time. In my case, I have access to the source code (non obfuscated), but I am not in control of adding the type hints (the are user defined). – emher Jul 05 '21 at 16:01

1 Answers1

1

The way to do this with static analysis is to declare the parameters as adhering to a protocol and then use mypy to validate that the actual parameters implement that protocol:

from typing import Protocol


class ValueProtocol(Protocol):
    value: str


class ValueThing:
    def __init__(self):
        self.value = "foo"


def display(some_object: ValueProtocol):
    print(some_object.value)


display(ValueThing())  # no errors, because ValueThing implements ValueProtocol
display("foo")  # mypy error: Argument 1 to "display" has incompatible type "str"; expected "ValueProtocol"

Doing this at runtime with mock objects is impossible to do in a generic way, because you can't be certain that the function will go through every possible code path; you would need to write a unit test with carefully constructed mock objects for each function and make sure that you maintain 100% code coverage.

Using type annotations and static analysis is much easier, because mypy (or similar tools) can check each branch of the function to make sure that the code is compatible with the declared type of the parameter, without having to generate fake values and actually execute the function against them.

If you want to programmatically inspect the annotations from someone else's module, you can use the magic __annotations__ attribute:

>>> display.__annotations__
{'some_object': <class '__main__.ValueProtocol'>, 'return': None}
Samwise
  • 68,105
  • 3
  • 30
  • 44
  • Do you know how IDEs are able to do it? PyCharm seems to be right more-or-less all the time. Generally, I agree that type annotations would be the way to go, but in the usecase spawning this question, I am not in control of adding the type hints (the functions are user defined). – emher Jul 05 '21 at 16:07
  • Does the actual code you're working with have type hints? – Samwise Jul 05 '21 at 16:47
  • No, the user-defined function that i am inspecting do not. – emher Jul 05 '21 at 16:48
  • I'd imagine then that the way to do it would be to make a copy of that function, annotate it with the type that you're actually passing into it, and see if you get an error within your copied function. Maybe that's what the IDE's doing behind the scenes to attempt to check untyped functions. – Samwise Jul 05 '21 at 16:49