Search code examples
pythonpycharmpython-decoratorspython-typing

Parameter 'self' unfilled for external wrapper for class methods


I'm working on my Python project in PyCharm 2022.3.3 (Community Edition). I have a wrapper for my class' methods which loads some data from the file before the method's execution and dumps it after. There are warnings when I use this wrapper.

Example:

import json

def load_and_save(method):
    def wrapper(self, *args, **kwargs):
        with open(self.filename, 'r') as f:
            self.data = json.load(f)

        result = method(self, *args, **kwargs)

        with open(self.filename, 'w') as f:
            json.dump(self.data, f)

        return result

    return wrapper

class MyClass:
    def __init__(self, filename):
        self.data = None
        self.filename = filename

    @load_and_save
    def my_method(self, x):
        self.data['value'] += x
        return self.data['value']

m = MyClass('data.json')
m.my_method(5)
           ^^^ Type 'int' doesn't have expected attributes 'filename', 'data'

This example shows the warning Type 'int' doesn't have expected attributes 'filename', 'data'

My original code in the same way shows Parameter 'self' unfilled

This example and my code also work fine. Is this my mistake or PyCharm's?

I'm not waiting for any warnings because this wrapper is ready to use self argument. I also create instances of both classes.

Update from 19/05/23:

OS: Mac OS Ventura 13.2.1

Screenshot of the warning in the console


Solution

  • If you are not providing any type annotations, you should not be surprised that your IDE has no idea what your intention is.

    Sure, PyCharm goes to great lengths to semi-statically analyze your code and infer, what types you expect in any given situation, but it has its limits.

    In this case, all you need to do is make the load_and_save decorator function explicitly generic, such that the return type is exactly the argument type:

    from typing import TypeVar
    
    T = TypeVar("T")
    
    def load_and_save(method: T) -> T: ...
    

    This should get rid of the warning because the type checker will now know that the decorated method retains its exact type signature.


    But I would highly recommend providing actually useful annotations for your entire code, not just selectively "patching" parts that the IDE cannot understand on its own.

    The problem with the above approach for example is that inside the decorator, there is still no bound on what type method can actually be, so there is no way to know, if you can even call it like a function.

    In general, no type annotations mean a static type checker will inevitably fall back to typing.Any everywhere. Type annotations force you to actually think about exactly what objects you want to be dealing with in any given situation. I won't descend into a rant here about why you should use them in every project that is at least half-serious. This is discussed extensively all over the place. Suffice it to say, properly typing your Python code is becoming best practice for a reason.

    Here is how I would actually annotate your code: (I simplified it for illustrative purposes)

    from collections.abc import Callable
    from typing import Concatenate, ParamSpec, TypeVar
    
    P = ParamSpec("P")
    R = TypeVar("R")
    T = TypeVar("T", bound="MyClass")
    
    
    def load_and_save(
        method: Callable[Concatenate[T, P], R]
    ) -> Callable[Concatenate[T, P], R]:
        def wrapper(self: T, /, *args: P.args, **kwargs: P.kwargs) -> R:
            print(f"Read from {self.filename=}")
            result = method(self, *args, **kwargs)
            print(f"Write to {self.filename=}")
            return result
        return wrapper
    
    
    class MyClass:
        def __init__(self, filename: str) -> None:
            self.data = 0
            self.filename = filename
    
        @load_and_save
        def my_method(self, x: int) -> int:
            self.data += x
            return self.data
    
    
    m = MyClass("data.json")
    m.my_method(5)
    

    Granted, decorators are typically one of the trickier things to annotate correctly. And ironically PyCharm seems to be bugged (at least my version) in that it still warns about the argument to my_method, expecting it to be of type T instead of int. But that is a problem with the internal type checker of PyCharm.

    If you check with mypy --strict for example, you can see that the code passes without error and the type of m.my_method is correctly inferred. (see this playground)

    For more on type hints read PEP 483, PEP 484 and the typing module documentation.