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
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.