How to use MaxLen of typing.Annotation of python 3.9?

Viewed 1467

I'm aware there's this new typing format Annotated where you can specify some metadata to the entry variables of a function. From the docs, you could specify the maximum length of a incoming list such as:

  • Annotated can be used with nested and generic aliases:
T = TypeVar('T')
Vec = Annotated[list[tuple[T, T]], MaxLen(10)]
V = Vec[int]

V == Annotated[list[tuple[int, int]], MaxLen(10)]

But I cannot finish to comprehend what MaxLen is. Are you supposed to import a class from somewhere else? I've tried importing typing.MaxLen but doesn't seems to exists (I'm using Python 3.9.6, which I think it should exist here...?).

Example code of what I imagined it should have worked:

from typing import List, Annotated, MaxLen

def function(foo: Annotated[List[int], MaxLen(10)]):
    # ...
    return True

Where can one find MaxLen?

EDIT:

It seems like MaxLen is some sort of class you have to create. The problem is that I cannot see how you should do it. Are there public examples? How can someone implement this function?

1 Answers

As stated by AntiNeutronicPlasma, Maxlen is just an example so you'll need to create it yourself.

Here's an example for how to create and parse a custom annotation such as MaxLen to get you started.

First, we define the annotation class itself. It's a very simple class, we only need to store the relevant metadata, in this case, the max value:

class MaxLen:
    def __init__(self, value):
        self.value = value

Now, we can define a function that uses this annotation, such as the following:

def sum_nums(nums: Annotated[List[int], MaxLen(10)]):
    return sum(nums)

But it's going to be of little use if nobody checks for it. So, one option could be to implement a decorator that checks your custom annotations at runtime. The functions get_type_hints, get_origin and get_args from the typing module are going to be your best friends. Below is an example of such a decorator, which parses and enforces the MaxLen annotation on list types:

from functools import wraps
from typing import get_type_hints, get_origin, get_args, Annotated

def check_annotations(func):
    @wraps(func)
    def wrapped(**kwargs):
        # perform runtime annotation checking
        # first, get type hints from function
        type_hints = get_type_hints(func, include_extras=True)
        for param, hint in type_hints.items():
            # only process annotated types
            if get_origin(hint) is not Annotated:
                continue
            # get base type and additional arguments
            hint_type, *hint_args = get_args(hint)
            # if a list type is detected, process the args
            if hint_type is list or get_origin(hint_type) is list:
                for arg in hint_args:
                    # if MaxLen arg is detected, process it
                    if isinstance(arg, MaxLen):
                        max_len = arg.value
                        actual_len = len(kwargs[param])
                        if actual_len > max_len:
                            raise ValueError(f"Parameter '{param}' cannot have a length "
                                             f"larger than {max_len} (got length {actual_len}).")
        # execute function once all checks passed
        return func(**kwargs)

    return wrapped

(Note that this particular example only works with keyword arguments, but you could probably find a way to make it work for normal arguments too).

Now, you can apply this decorator to any function, and your custom annotation will get parsed:

from typing import Annotated, List

@check_annotations
def sum_nums_strict(nums: Annotated[List[int], MaxLen(10)]):
    return sum(nums)

Below is an example of the code in action:

>>> sum_nums(nums=list(range(5)))
10
>>> sum_nums(nums=list(range(15)))
105
>>> sum_nums_strict(nums=list(range(5)))
10
>>> sum_nums_strict(nums=list(range(15)))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "annotated_test.py", line 29, in wrapped
    raise ValueError(f"Parameter '{param}' cannot have a length "
ValueError: Parameter 'nums' cannot have a length larger than 10 (got length 15).
Related