Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why isn't this function type-annotated correctly (error: Missing type parameters for generic type)?

Is this function type-annotated correctly?

import subprocess
from os import PathLike
from typing import Union, Sequence, Any


def run(shell_command: Union[bytes, str, Sequence[Union[bytes, str, PathLike]]], **subprocess_run_kwargs: Any) -> int:
    return subprocess.run(shell_command, check=True, shell=True, **subprocess_run_kwargs).returncode

I'm guessing it's not, because I'm getting:

he\other.py:6: error: Missing type parameters for generic type

To get the same error, then save the above code in other.py, and then:

$ pip install mypy

$ mypy --strict other.py
like image 598
Laurențiu Andronache Avatar asked Mar 09 '19 11:03

Laurențiu Andronache


People also ask

What is type annotation in Python?

Type annotations — also known as type signatures — are used to indicate the datatypes of variables and input/outputs of functions and methods. In many languages, datatypes are explicitly stated. In these languages, if you don't declare your datatype — the code will not run.

How do I ignore MYPY errors?

You can use a special comment # type: ignore[code, ...] to only ignore errors with a specific error code (or codes) on a particular line. This can be used even if you have not configured mypy to show error codes. Currently it's only possible to disable arbitrary error codes on individual lines using this comment.

What is MYPY?

“Mypy is an optional static type checker for Python that aims to combine the benefits of dynamic (or 'duck') typing and static typing. Mypy combines the expressive power and convenience of Python with a powerful type system and compile-time type checking.” A little background on the Mypy project.


2 Answers

As described in sanyash's answer, os.PathLike is defined as a generic type. You can look at the stub in the typeshed repo. However, os.PathLike is only generic in the stub file, the implementation importable from os is not.

Not specifying the type var parameter (path: PathLike) leads to a mypy error. Specifying the type var parameter (path: PathLike[Any]) leads to a Python interpreter (runtime) error.

This exact issue has been raised in the mypy repo as #5667. As a result PR #5833 extends the mypy documentation:

  • https://mypy.readthedocs.io/en/latest/runtime_troubles.html#using-classes-that-are-generic-in-stubs-but-not-at-runtime

The added section points out three ways to handle this:

  1. Interpretation of the annotation by the Python interpreter (at runtime) can be disabled with a special from __future__ import annotations import, see here. This is planned to become the default in Python 3.10 and solves a bunch of annotation related issues.

    from __future__ import annotations
    from os import PathLike
    import subprocess
    from typing import Any, Sequence, Union
    
    def run(shell_command: Union[bytes, str, Sequence[Union[bytes, str, PathLike[Any]]]], **subprocess_run_kwargs: Any) -> int:
         return subprocess.run(shell_command, check=True, shell=True, **subprocess_run_kwargs).returncode
    
  2. Use typing.TYPE_CHECKING to supply different annotations depending on whether a type checker or the Python interpreter (runtime) interprets the file.

    from os import PathLike
    import subprocess
    from typing import Any, Sequence, TYPE_CHECKING, Union
    
    if TYPE_CHECKING:
        BasePathLike = PathLike[Any]
    else:
        BasePathLike = PathLike
    
    def run(shell_command: Union[bytes, str, Sequence[Union[bytes, str, BasePathLike]]], **subprocess_run_kwargs: Any) -> int:
        return subprocess.run(shell_command, check=True, shell=True, **subprocess_run_kwargs).returncode
    
  3. Supply the annotation as a string. The Python interpreter (runtime) will not interpret the annotation, but mypy picks up the proper meaning.

    import subprocess
    from typing import Any, Sequence, Union
    
    def run(shell_command: Union[bytes, str, Sequence[Union[bytes, str, 'PathLike[Any]']]], **subprocess_run_kwargs: Any) -> int:
        return subprocess.run(shell_command, check=True, shell=True, **subprocess_run_kwargs).returncode
    
like image 60
Seoester Avatar answered Oct 19 '22 17:10

Seoester


PathLike is a generic type, so you need to use it with a type parameter (AnyStr for example):

import subprocess
from os import PathLike
from typing import Union, Sequence, Any, AnyStr


def run(shell_command: Union[bytes, str, Sequence[Union[bytes, str, PathLike[AnyStr]]]], **subprocess_run_kwargs: Any) -> int:
    return subprocess.run(shell_command, check=True, shell=True, **subprocess_run_kwargs).returncode

Related issues:

  • https://github.com/python/mypy/issues/6112
  • https://github.com/python/mypy/issues/6128

UPDATE

Sorry, I didn't check this code at runtime. With some tricks it is possible to write a workaround:

import subprocess
from os import PathLike as BasePathLike
from typing import Union, Sequence, Any, AnyStr, TYPE_CHECKING
import abc


if TYPE_CHECKING:
    PathLike = BasePathLike
else:
    class FakeGenericMeta(abc.ABCMeta):
        def __getitem__(self, item):
            return self

    class PathLike(BasePathLike, metaclass=FakeGenericMeta):
        pass

def run(shell_command: Union[bytes, str, Sequence[Union[bytes, str, PathLike[AnyStr]]]], **subprocess_run_kwargs: Any) -> int:
    return subprocess.run(shell_command, check=True, shell=True, **subprocess_run_kwargs).returncode

Issues related to this workaround:

  • mypy: how to define a generic subclass
  • https://github.com/python/mypy/issues/5264
like image 45
sanyassh Avatar answered Oct 19 '22 16:10

sanyassh