10

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 an 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 fully comprehend what MaxLen is. Are you supposed to import a class from somewhere else? I've tried importing typing.MaxLen but it doesn't seem to exist (I'm using Python 3.9.6, which I think it should exist here...?).

Example code of what I imagined 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
  • 3
    Those are just examples, demonstrating what can be done. See this question for a similar discussion. Commented Jul 20, 2021 at 11:42

1 Answer 1

14

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).
Sign up to request clarification or add additional context in comments.

The whole point of the "typing" module is static hints. What you are showing is dynamic (runtime) checks. Can you provide an evidence that it's the intended way of use (maybe a link to docs, pep or something)?
PEP 593: "This PEP adds an Annotated type to the typing module to decorate existing types with context-specific metadata. (...) This metadata can be used for either static analysis or at runtime." As far as I am aware, no IDE currently implements any static analysis using Annotated type hints, nor does it seem feasible for the given examples (list length, integer range...), since their values will generally only be known at runtime.
Thanks for the clarification. I think it would be possible to implement static type checks for things like list length for simple cases where we know the length for sure. For example, when we define a list using a literal. Anyway, this is indeed too much to ask from Python (and its type checkers), unfortunately.

Your Answer

Draft saved
Draft discarded

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.