Python type hints and context managers

后端 未结 4 1790
执念已碎
执念已碎 2020-12-15 15:05

How should a context manager be annotated with Python type hints?

import typing

@contextlib.contextmanager
def foo() -> ???:
    yield

相关标签:
4条回答
  • 2020-12-15 15:40

    The Iterator[] version doesn't work when you want to return the contextmanager's reference. For instance, the following code:

    from typing import Iterator
    
    def assert_faster_than(seconds: float) -> Iterator[None]:
        return assert_timing(high=seconds)
    
    @contextmanager
    def assert_timing(low: float = 0, high: float = None) -> Iterator[None]:
        ...
    

    Will produce an error on the return assert_timing(high=seconds) line:

    Incompatible return value type (got "_GeneratorContextManager[None]", expected "Iterator[None]")

    Any legit usage of the function:

    with assert_faster_than(1):
        be_quick()
    

    Will result in something like this:

    "Iterator[None]" has no attribute "__enter__"; maybe "__iter__"?
    "Iterator[None]" has no attribute "__exit__"; maybe "__next__"?
    "Iterator[None]" has no attribute "__enter__"; maybe "__iter__"?
    "Iterator[None]" has no attribute "__exit__"; maybe "__next__"?
    

    You could fix it like this...

    def assert_faster_than(...) -> Iterator[None]:
        with assert_timing(...):
            yield
    

    But I am going to use the new ContextManager[] object instead and silence out mypy for the decorator:

    from typing import ContextManager
    
    def assert_faster_than(seconds: float) -> ContextManager[None]:
        return assert_timing(high=seconds)
    
    @contextmanager  # type: ignore
    def assert_timing(low: float = 0, high: float = None) -> ContextManager[None]:
        ...
    
    0 讨论(0)
  • 2020-12-15 15:52

    With my PyCharm, I do the following to make its type hinting work:

    from contextlib import contextmanager
    from typing import ContextManager
    
    @contextmanager
    def session() -> ContextManager[Session]:
        yield Session(...)
    
    0 讨论(0)
  • 2020-12-15 15:58

    Whenever I'm not 100% sure what types a function accepts, I like to consult typeshed, which is the canonical repository of type hints for Python. Mypy directly bundles and uses typeshed to help it perform its typechecking, for example.

    We can find the stubs for contextlib here: https://github.com/python/typeshed/blob/master/stdlib/2and3/contextlib.pyi

    if sys.version_info >= (3, 2):
        class GeneratorContextManager(ContextManager[_T], Generic[_T]):
            def __call__(self, func: Callable[..., _T]) -> Callable[..., _T]: ...
        def contextmanager(func: Callable[..., Iterator[_T]]) -> Callable[..., GeneratorContextManager[_T]]: ...
    else:
        def contextmanager(func: Callable[..., Iterator[_T]]) -> Callable[..., ContextManager[_T]]: ...
    

    It's a little overwhelming, but the line we care about is this one:

    def contextmanager(func: Callable[..., Iterator[_T]]) -> Callable[..., ContextManager[_T]]: ...
    

    It states that the decorator takes in a Callable[..., Iterator[_T]] -- a function with arbitrary arguments returning some iterator. So in conclusion, it would be fine to do:

    @contextlib.contextmanager
    def foo() -> Iterator[None]:
        yield
    

    So, why does using Generator[None, None, None] also work, as suggested by the comments?

    It's because Generator is a subtype of Iterator -- we can again check this for ourselves by consulting typeshed. So, if our function returns a generator, it's still compatible with what contextmanager expects so mypy accepts it without an issue.

    0 讨论(0)
  • 2020-12-15 15:59

    The return type of the function wrapped by a context manager is Iterator[None].

    from contextlib import contextmanager
    from typing import Iterator
    
    @contextmanager
    def foo() -> Iterator[None]:
        yield
    
    0 讨论(0)
提交回复
热议问题