Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python type hints and context managers

Tags:

python

mypy

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

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

The documentation on contextlib doesn't mention types much.

The documentation on typing.ContextManager is not all that helpful either.

There's also typing.Generator, which at least has an example. Does that mean I should use typing.Generator[None, None, None] and not typing.ContextManager?

import typing  @contextlib.contextmanager def foo() -> typing.Generator[None, None, None]:     yield 
like image 966
Peter Avatar asked Apr 09 '18 13:04

Peter


People also ask

What are Python context managers?

Context managers allow you to allocate and release resources precisely when you want to. The most widely used example of context managers is the with statement. Suppose you have two related operations which you'd like to execute as a pair, with a block of code in between.

What are Python type hints?

Python's type hints provide you with optional static typing to leverage the best of both static and dynamic typing. Besides the str type, you can use other built-in types such as int , float , bool , and bytes for type hintings. To check the syntax for type hints, you need to use a static type checker tool.

Does Python enforce type hints?

Unlike how types work in most other statically typed languages, type hints by themselves don't cause Python to enforce types. As the name says, type hints just suggest types. There are other tools, which you'll see later, that perform static type checking using type hints.


1 Answers

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

like image 130
Michael0x2a Avatar answered Sep 20 '22 00:09

Michael0x2a