2025, Oct 20 11:00

Derive Python type aliases from class annotations without AttributeError: using __annotations__, TypeAliasType and Annotated

Learn how to derive a Python type alias from a class annotation, avoid AttributeError for instance attributes, and use __annotations__, TypeAliasType Annotated.

When you model object references as typed IDs—mirroring RDF-like URI references—it's tempting to pull a field type straight off a class. But instance annotations aren't class attributes, and reaching for them directly leads to a dead end. Here's a practical pattern for deriving a type alias from a class's annotated field without tripping over AttributeError.

Problem setup

The goal is to model a class and a non-literal attribute as a typed identifier, then build a type alias that mirrors the field's annotation. The example below uses a typed integer for the ID and attaches metadata with Annotated for downstream validation around serialization/deserialization.

import inspect
import typing
from typing import Annotated, Any, NewType, TypeAliasType, get_args, get_origin

UidType = NewType("UidType", int)

class Human:
    key: UidType
    ancestors: list["HumanId"]

def slot_info(model_cls: type, field_name: str) -> tuple[type, str, Any]:
    hints = inspect.get_annotations(model_cls)
    assert field_name in hints
    return (model_cls, field_name, hints[field_name])

HumanId = TypeAliasType("HumanId", Annotated[UidType, slot_info(Human, "key")])

assert Human.__name__ == "Human"
assert isinstance(HumanId, typing.TypeAliasType)
orig = get_origin(HumanId.__value__)
params = get_args(HumanId.__value__)
assert orig is Annotated
assert params[0] is UidType
assert params[1] == (Human, "key", UidType)
assert params[0] == params[1][2]

A seemingly direct alias like the next line will not work, because the field is not a class attribute:

HumanId = TypeAliasType("HumanId", Human.key)  # AttributeError: type object 'Human' has no attribute 'key'

Why this fails

If the member is only annotated as an instance attribute, it does not exist on the class. The type lives in the class's annotations mapping, or on instances once assigned, but not as a class attribute.

In other words, key is an instance-level annotation. Python records that information in the class’s __annotations__ but does not create a class attribute named key. Accessing Human.key will therefore raise AttributeError.

Working solution

Derive the alias from the class’s annotations dictionary. This lets you reference exactly the type that was used in the annotation.

from typing import NewType

UidType = NewType("UidType", int)

class Human:
    key: UidType
    ancestors: list["HumanId"]

HumanId = Human.__annotations__["key"]

For Python 3.12+, the same approach plays nicely with TypeAliasType:

from typing import NewType, TypeAliasType

UidType = NewType("UidType", int)

class Human:
    key: UidType
    ancestors: list["HumanId"]

HumanId = TypeAliasType("HumanId", Human.__annotations__["key"])

If you need to attach metadata, wrap the underlying type with Annotated. The metadata can embed whatever you need to validate that the annotation matches the field being referenced.

from typing import Annotated, NewType, TypeAliasType

UidType = NewType("UidType", int)

class Human:
    key: UidType
    ancestors: list["HumanId"]

def slot_meta(model_cls: type, field_name: str):
    hints = model_cls.__annotations__
    assert field_name in hints
    return (model_cls, field_name, hints[field_name])

HumanId = TypeAliasType("HumanId", Annotated[UidType, slot_meta(Human, "key")])

Why you should care

When you validate types around database serialization and deserialization, you want certainty that a type alias reflects the actual field annotation. Pulling the type from __annotations__ gives you that alignment and avoids brittle attribute lookups that will fail for instance annotations. With TypeAliasType and Annotated, you can also keep metadata alongside the real underlying type in a way that’s introspectable for your checks.

Takeaways

Don’t access instance annotations as class attributes. Instead, read them from the class’s __annotations__ mapping and, on Python 3.12+, wrap them with TypeAliasType if you want an explicit alias. When metadata matters, use Annotated to keep the extra context attached to the type. For background and formal details, see PEP 526 on variable annotations, PEP 613 on explicit type aliases, and the Python docs for Class __annotations__, typing.TypeAliasType, and typing.Annotated.

The article is based on a question from StackOverflow by ticapix and an answer by Sithila Sihan Somaratne.