Closed EpicBirb closed 3 months ago
Pyright (the static type checker upon which pylance is based) is working correctly here.
I'm not sure what you mean by "enforce the type of the decorated function". The type of the undecorated function in this case is (r: Any) -> None
. The Any
comes from the fact that parameter r
has no type annotation. The return type None
can be inferred. The type of the decorator is (func: Callable[[str], None]) -> Callable[[str], None]
. The return type is inferred. When the decorator is applied to the undecorated function, the resulting type is Callable[[str], None]
. This is a callable type that accepts one positional argument that is compatible with str
.
If you enable type checking in pylance (i.e. set typeCheckingMode
to "basic" or "standard") and attempt to call a
with an argument with an incompatible type, you will see a error from the type checker.
Code sample in pyright playground
from typing import Callable
def s(func: Callable[[str], None]):
return func
@s
def a(r):
...
a("") # No error
a(1) # Type error
Allow me to clear up a misunderstanding here. I know that calling the function will use type Callable[[str], None]
. But at the declaration of function a
(provided in the example above), r
is presumed as Unknown
by the type checker, however we know for a fact that the decorator asks for the function to be of type Callable[[str], None]
. So why can't it infer the argument r
at declaration to be of type str
and not Unknown
. Example:
Hovering over r
, I should expected it to be type annotated as str
, not Unknown
Static type analysis doesn't work that way. Inferring the type of an argument from a function call would be backward. Static type checkers can infer the return type of a call expression from the types of the arguments and the signature of the callee, but the converse (inferring the types of arguments from the call) isn't generally possible.
A decorator is just a special syntax for a call expression. Your code is effectively equivalent to:
def _a(r): ...
a = s(_a)
An expression (such as s(_a)
) is first translated to a parse tree where each node represents an operator and has references to its subexpressions. When evaluating the type of an expression, a type checker starts with the leaf nodes of this parse tree and evaluates those types first. Then it successively evaluates the types of the expressions that depend on those subexpressions until it evaluates the type of the entire tree. During this process, it reports any "type violations" — i.e. types of operands that are not allowed based on the operator type of an expression.
When a call expression (such as s(_a)
) is statically evaluated, the types of its argument expressions are evaluated first. The type checker then verifies that these argument types match the signature of the callee's signature. Call signatures can be very complex in Python — e.g. with *args
and **kwargs
parameters, positional-only and keyword-only parameters, default (optional) values, and overloads.
Deriving the type of an argument expression based on the signature of the call is not generally possible for a number of reasons. For one, there are many types that could be compatible with the callee's signature. For example, a
could be (str) -> None
or (object) -> None
. Both of these would satisfy the type constraints of s
. And this is a toy sample. In real-world examples, the number of compatible argument types would be immense — effectively infinite. Guessing which one the programmer intended would be computationally expensive and produce the wrong guess much of the time.
In your code sample, if your intent is for parameter r
in function a
to be limited to type str
(or a subclass thereof), you should add an explicit type annotation for r
indicating as such. That's the purpose of static type annotations in Python. A static type checker like pyright will then verify that the decorator you apply to this function is compatible with it.
Pyright is a standards-based type checker, and it conforms to the Python typing spec.
If you want to understand more about how static type analysis and type checking works, here are some documentation sources that you may find useful: Pyright Documentation Python Type Checking Specification
The decorated function
a
should have the parameterr
be inferred asstr
as stated in the decorators
. At the declaration ofa
, parameterr
is of typeUnknown
. However when we call functiona
, it does tell us thata
requires an object of typestr
.The decorator should be able to enforce the type of the decorated function as the decorator requires the function to be of type
Callable[[str], None]
.