Type Hint for a Function That Returns Only a Specific Set of Values

Type hint for a function that returns only a specific set of values

You can do that with literal types.

from typing_extensions import Literal
# from typing import Literal # Python 3.8 or higher

def fun(b: int) -> Literal["a", "b", "c"]:
if b == 0:
return "a"
if b == 1:
return "b"
return "d"

mypy is able to detect the return "d" as a invalid statement:

error: Incompatible return value type (got "Literal['d']",
expected "Union[Literal['a'], Literal['b'], Literal['c']]")

Python 3.8

Thanks to the PEP 586, the Literal is already included by default in the Python 3.8 typing module.

Type hint for a list of possible values

I think you want a literal type:

def func(mode="a": Literal["a", "b"]):
if mode not in ["a", "b"]:
raise AttributeError("not ok")

This was introduced in Python 3.8, via PEP 586.

Python 3 type hint for string options

Option 1: Literal

You can do that with literal types.

from typing import Literal
# from typing_extensions import Literal # Python 3.7 or below

def func(method: Literal['simple_method', 'some_other_method']):
...

Python 3.8

Thanks to the PEP 586, the Literal is already included by default in the Python 3.8 typing module.

Option 2: Enum

If you'd rather not use type hints, you could also consider enums like so:

from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3

Once you have an enum with all the possible choices, you can hint the function in order to accept only your custom enum. More info here

Example:

from typing import NewType

Colors = NewType('Colors', Color)

How do I type hint a method with the type of the enclosing class?

TL;DR: As of today (2019), in Python 3.7+ you can turn this feature on using a "future" statement, from __future__ import annotations.

(The behaviour enabled by from __future__ import annotations might become the default in future versions of Python, and was going to be made the default in Python 3.10. However, the change in 3.10 was reverted at the last minute, and now may not happen at all.)

In Python 3.6 or below, you should use a string.


I guess you got this exception:

NameError: name 'Position' is not defined

This is because Position must be defined before you can use it in an annotation, unless you are using Python with PEP 563 changes enabled.

Python 3.7+: from __future__ import annotations

Python 3.7 introduces PEP 563: postponed evaluation of annotations. A module that uses the future statement from __future__ import annotations will store annotations as strings automatically:

from __future__ import annotations

class Position:
def __add__(self, other: Position) -> Position:
...

This had been scheduled to become the default in Python 3.10, but this change has now been postponed. Since Python still is a dynamically typed language so no type-checking is done at runtime, typing annotations should have no performance impact, right? Wrong! Before Python 3.7, the typing module used to be one of the slowest python modules in core so for code that involves importing the typing module, you will see an up to 7 times increase in performance when you upgrade to 3.7.

Python <3.7: use a string

According to PEP 484, you should use a string instead of the class itself:

class Position:
...
def __add__(self, other: 'Position') -> 'Position':
...

If you use the Django framework, this may be familiar, as Django models also use strings for forward references (foreign key definitions where the foreign model is self or is not declared yet). This should work with Pycharm and other tools.

Sources

The relevant parts of PEP 484 and PEP 563, to spare you the trip:

Forward references

When a type hint contains names that have not been defined yet, that definition may be expressed as a string literal, to be resolved later.

A situation where this occurs commonly is the definition of a container class, where the class being defined occurs in the signature of some of the methods. For example, the following code (the start of a simple binary tree implementation) does not work:

class Tree:
def __init__(self, left: Tree, right: Tree):
self.left = left
self.right = right

To address this, we write:

class Tree:
def __init__(self, left: 'Tree', right: 'Tree'):
self.left = left
self.right = right

The string literal should contain a valid Python expression (i.e., compile(lit, '', 'eval') should be a valid code object) and it should evaluate without errors once the module has been fully loaded. The local and global namespace in which it is evaluated should be the same namespaces in which default arguments to the same function would be evaluated.

and PEP 563:

Implementation

In Python 3.10, function and variable annotations will no longer be evaluated at definition time. Instead, a string form will be preserved in the respective __annotations__ dictionary. Static type checkers will see no difference in behavior, whereas tools using annotations at runtime will have to perform postponed evaluation.

...

Enabling the future behavior in Python 3.7

The functionality described above can be enabled starting from Python 3.7 using the following special import:

from __future__ import annotations

Things that you may be tempted to do instead

A. Define a dummy Position

Before the class definition, place a dummy definition:

class Position(object):
pass

class Position(object):
...

This will get rid of the NameError and may even look OK:

>>> Position.__add__.__annotations__
{'other': __main__.Position, 'return': __main__.Position}

But is it?

>>> for k, v in Position.__add__.__annotations__.items():
... print(k, 'is Position:', v is Position)
return is Position: False
other is Position: False

B. Monkey-patch in order to add the annotations:

You may want to try some Python metaprogramming magic and write a decorator
to monkey-patch the class definition in order to add annotations:

class Position:
...
def __add__(self, other):
return self.__class__(self.x + other.x, self.y + other.y)

The decorator should be responsible for the equivalent of this:

Position.__add__.__annotations__['return'] = Position
Position.__add__.__annotations__['other'] = Position

At least it seems right:

>>> for k, v in Position.__add__.__annotations__.items():
... print(k, 'is Position:', v is Position)
return is Position: True
other is Position: True

Probably too much trouble.

How can I specify the function type in my type hints?

As @jonrsharpe noted in a comment, this can be done with typing.Callable:

from typing import Callable

def my_function(func: Callable):

Note: Callable on its own is equivalent to Callable[..., Any].
Such a Callable takes any number and type of arguments (...) and returns a value of any type (Any). If this is too unconstrained, one may also specify the types of the input argument list and return type.

For example, given:

def sum(a: int, b: int) -> int: return a+b

The corresponding annotation is:

Callable[[int, int], int]

That is, the parameters are sub-scripted in the outer subscription with the return type as the second element in the outer subscription. In general:

Callable[[ParamType1, ParamType2, .., ParamTypeN], ReturnType]

How to annotate types of multiple return values?

You are always returning one object; using return one, two simply returns a tuple.

So yes, -> Tuple[bool, str] is entirely correct.

Only the Tuple type lets you specify a fixed number of elements, each with a distinct type. You really should be returning a tuple, always, if your function produces a fixed number of return values, especially when those values are specific, distinct types.

Other sequence types are expected to have one type specification for a variable number of elements, so typing.Sequence is not suitable here. Also see What's the difference between lists and tuples?

Tuples are heterogeneous data structures (i.e., their entries have different meanings), while lists are homogeneous sequences. Tuples have structure, lists have order.

Python's type hint system adheres to that philosophy, there is currently no syntax to specify an iterable of fixed length and containing specific types at specific positions.

If you must specify that any iterable will do, then the best you can do is:

-> Iterable[Union[bool, str]]

at which point the caller can expect booleans and strings in any order, and of unknown length (anywhere between 0 and infinity).

Last but not least, as of Python 3.9, you can use

-> tuple[bool, str]

instead of -> Tuple[bool, str]; support for type hinting notation has been added to most standard-library container types (see PEP 585 for the complete list). In fact, you can use this as of Python 3.7 too provided you use the from __future__ import annotations compiler switch for your modules and a type checker that supports the syntax.



Related Topics



Leave a reply



Submit