Skip to content

Final Types

Control whether a composite type is recursively expanded or treated as an atomic value. Three approaches can be combined freely.

Approach 1: Constructor Parameter

@dataclass
class Money:
    amount: float
    currency: str

@dataclass
class Product:
    name: str
    price: Money

extractor = MetadataExtractor(final_types={Money})
metadata = extractor.extract(Product)

assert "price" in metadata
assert "price.amount" not in metadata   # not expanded
assert metadata["price"].composite is False
assert metadata["price"].final is True

Approach 2: @final_type Decorator

from fields_metadata import final_type

@final_type
@dataclass
class Money:
    amount: float
    currency: str

extractor = MetadataExtractor()   # no final_types parameter needed
metadata = extractor.extract(Product)

assert "price.amount" not in metadata

Approach 3: FinalType Annotation (per-field)

from typing import Annotated
from fields_metadata import FinalType

@dataclass
class Product:
    regular_price: Money                          # expanded
    special_price: Annotated[Money, FinalType()]  # not expanded

metadata = MetadataExtractor().extract(Product)

assert "regular_price.amount" in metadata
assert "special_price.amount" not in metadata

Works with containers and optional types:

@dataclass
class Document:
    tags: list[Annotated[Tag, FinalType()]]
    author: Annotated[Author, FinalType()] | None

Understanding final

A field is final=True when it has no non-derived subfields:

  • Primitive types are always final.
  • Composite types marked as final are final.
  • Derived fields are always final.
extractor = MetadataExtractor()
metadata = extractor.extract(Product)

assert metadata["name"].final is True          # primitive
assert metadata["price"].final is False        # has subfields

extractor2 = MetadataExtractor(final_types={Money})
metadata2 = extractor2.extract(Product)

assert metadata2["price"].final is True        # now treated as atomic