How am I supposed to use Annotated?

PEP 593 introduced typing.Annotated. I’ve been trying to wrap my head around how it’s supposed to be used, and I can’t make sense of it. Having read PEP 593, the discussions that preceded it [1][2], and the documentation on Annotated, I still do not understand how Annotated is meant to be used.

The first example in the PEP is

UnsignedShort = Annotated[int, struct2.ctype('H')]
SignedChar = Annotated[int, struct2.ctype('b')]

class Student(struct2.Packed):
    # mypy typechecks 'name' field as 'str'
    name: Annotated[str, struct2.ctype("<10s")]
    serialnum: UnsignedShort
    school: SignedChar

# 'unpack' only uses the metadata within the type annotations
Student.unpack(record)
# Student(name=b'raymond   ', serialnum=4658, school=264)

The semantics of struct2 are not made clear, which prevents me from understanding the example [1].

I have tried to recreate something that behaves kind of like this example, and this is what I came up with:

import struct
from dataclasses import dataclass
from typing import Annotated

UnsignedShort = Annotated[int, struct.Struct("H")]
SignedChar = Annotated[int, struct.Struct("b")]


@dataclass
class Student:
    name: Annotated[str, struct.Struct("<10s")]
    serialnum: UnsignedShort
    school: SignedChar

    @classmethod
    def unpack(cls, record: bytes) -> "Student":
        unpacker = struct.Struct(
            "".join(a.__metadata__[0].format for a in cls.__annotations__.values())
        )
        return cls(*unpacker.unpack(record))


record = b"raymond   2\x12@"
Student.unpack(record)
# Student(name=b'raymond   ', serialnum=4658, school=64)

which works, but this line

"".join(a.__metadata__[0].format for a in cls.__annotations__.values())

is all kinds of awful. Is this really how I’m supposed to use Annotated at runtime?


  1. Like, is Student some kind of dataclass? name, serialnumber, and school do not seem to be class vars, despite syntactically appearing to be. struct2.Packed apparently provides an unpack method which, per the example, “only uses the metadata within the type annotations”. How? Also, school=264 does not fit in a SignedChar. ↩︎

They might not be used at runtime at all. They are available, but they might also just be for use by a typechecker that knows to look for them.

Do such typecheckers exist?

PEP 593 says that

how to interpret the metadata (if at all) is the responsibility of the tool or library encountering the Annotated type.

It would help my understanding of Annotated if concrete examples of its use were provided.

As is, it is not clear to me what tangible benefits option 1 below has over option 2.

# Option 1
some_data: Annotated[int, UnsignedShort]
# Option 2
some_data: int  # This is an unsigned short.

I think the point is that there isn’t a fixed use case for it and that you can use the extra data for anything you like.

If you don’t have a use for any extra information right now beyond int then just use int and not Annotated.

From the example you started with:

Annotated[int, struct.Struct("H")]

tells the Python type-checker that it’s an int but also provides some information about a C representation. This might be useful if you’re writing it to a file or sending it to some external library with ctypes for example.

Making use of that information would be up to your program.


Slightly tangential commentary:

At some point in the distant past, annotations were free-form and unspecified and you could use them for whatever you wanted. Then it was decided that they must be about types, and must be in a strict format, and you must open lots of issues with whatever libraries you depend on to complain that they haven’t provided annotations.

Annotated is just a way of restoring some of that original flexibility by providing a place to put arbitrary extra information that’s useful to you (and maybe no-one else)

1 Like