The Flyte Type Engine
When you pass complex Python data structures between Flyte tasks, Flyte cannot simply pass in-memory Python objects because tasks may run in isolated containers across different machines or execution environments. The flyte-sdk type system solves this by translating Python types and values into Flyte IDL primitives (LiteralType, Literal, and LiteralMap), validating type contracts statically and dynamically, and deserializing them back into Python types on task invocation.
At the center of this mechanism is TypeEngine and its extensible transformer architecture based on TypeTransformer.
Core Architecture and Data Flow
In flyte-sdk, type translation operates in two distinct phases:
- Interface Compilation (Static Analysis):
TypeEngine.to_literal_type(python_type)inspects task signatures and returns a ProtobufLiteralTypedefining the input and output interface for FlytePropeller and the Flyte control plane. - Runtime Execution (Serialization & Deserialization):
- When a task completes,
TypeEngine.to_literal(python_val, python_type, expected)serializes the Python output object into a ProtobufLiteral. - When a downstream task executes,
TypeEngine.to_python_value(lv, expected_python_type)deserializes the incomingLiteralback into the required Python object.
- When a task completes,
+-----------------------------------------------------------------------------------------+
| TypeEngine |
| |
| Python Type / Annotation Flyte IDL Representation |
| +------------------------+ +--------------------------+ |
| | int, str, float, bool | to_literal_type() | LiteralType | |
| | List[T], Dict[str, V] | ---------------------> | (simple, collection, | |
| | Dataclass, BaseModel | | map, struct, union...) | |
| +------------------------+ +--------------------------+ |
| | | |
| | to_literal() | to_python_value() |
| v v |
| +------------------------+ +--------------------------+ |
| | Python Instance / Val | ---------------------> | Literal / LiteralMap | |
| | (e.g., File("...")) | <--------------------- | (scalar.primitive, blob,| |
| +------------------------+ | scalar.binary, map...) | |
+-----------------------------------------------------------------------------------------+
The Transformer Resolution Algorithm
When TypeEngine.get_transformer(python_type) searches for an appropriate transformer, it evaluates candidates in types/_type_engine.py in the following order:
- Annotated Types: If wrapped in
typing.Annotated,TypeEngineinspects metadata arguments. If an explicitTypeTransformerinstance is present in the annotation arguments, it returns that transformer; otherwise, it resolves against the underlying wrapped type (args[0]). - Enums: Subclasses of
enum.Enumare explicitly matched to_ENUM_TRANSFORMER(avoiding accidental matching to string transformers if the enum inherits fromstr). - Generic Types: If
python_typecontains__origin__,TypeEnginefirst checks if the full generic type is registered in_REGISTRY, and falls back to checkingpython_type.__origin__. - PEP 604 Union Types:
types.UnionTypemaps to the registered union transformer. - Direct Registry Match: Exact key lookup in
_REGISTRY. - Method Resolution Order (MRO): Walks
inspect.getmro(python_type)to check if a base class has a registered transformer. - Dataclass Fallback: If
dataclasses.is_dataclass(python_type)is true, returns_DATACLASS_TRANSFORMER. - Pickle Fallback: If no transformer matches,
TypeEnginelogs a warning (display_pickle_warning) and returns an instance ofFlytePickleTransformer.
Built-In Type Transformers
flyte-sdk registers transformers for standard Python types during module initialization in types/_type_engine.py:
Primitives and Simple Types
SimpleTransformer handles standard Python scalar types using direct mappings:
int->SimpleType.INTEGER(Literal.scalar.primitive.integer)float->SimpleType.FLOAT(Literal.scalar.primitive.float_value)str->SimpleType.STRING(Literal.scalar.primitive.string_value)bool->SimpleType.BOOLEAN(Literal.scalar.primitive.boolean)datetime.datetime->SimpleType.DATETIMEdatetime.timedelta->SimpleType.DURATIONNone/NoneType->SimpleType.NONE
Dataclasses and Pydantic Models
- Dataclasses:
DataclassTransformerusesmashumaro(MessagePackEncoderandMessagePackDecoder) to serialize dataclass instances to MessagePack binary payloads wrapped in aLiteral(scalar=Scalar(binary=Binary(value=msgpack_bytes, tag="msgpack"))). It also extracts JSON Schema definitions usingmashumaro.jsonschema.build_json_schemafor backend interface metadata. - Pydantic Models:
PydanticTransformerconverts subclasses ofpydantic.BaseModelinto MessagePack binary IDL literals and generates JSON Schema metadata viaBaseModel.model_json_schema().
Collections and Containers
- Lists (
ListTransformer): Supports univariatetyping.List[T]. Transforms each element recursively usingTypeEngine.to_literaland packages elements into a ProtobufLiteralCollection. - Dictionaries (
DictTransformer):- Typed dictionaries with string keys (
Dict[str, T]) serialize intoLiteralMapwithmap_value_type. - Untyped dictionaries or dictionaries with non-string keys serialize into binary MessagePack structs via
dict_to_binary_literal.
- Typed dictionaries with string keys (
- Unions (
UnionTransformer): Supportstyping.Union[T1, T2, ...]. Serializes values into aScalar.unionliteral containing the inner literal and a tag identifying the selected variant. If a Python value matches multiple union variants structurally,UnionTransformerraises aTypeErrorindicating ambiguous choices.
Files, Directories, and Blobs
Types like File (in io/_file.py) and Dir (in io/_dir.py) register specialized transformers that map instances to types_pb2.BlobType and literals_pb2.Blob containing URI references to remote or local storage.
Implementing and Registering Custom Type Transformers
To support custom domain objects, subclass TypeTransformer[T] and register your class with TypeEngine.register().
Step 1: Subclass TypeTransformer
Implement the required abstract methods: get_literal_type, to_literal, and to_python_value. You can optionally override guess_python_type and assert_type. The following example demonstrates how FileTransformer in io/_file.py implements this interface:
from pathlib import Path
from typing import Type
from flyteidl.core import literals_pb2, types_pb2
from flyte.io import File
from flyte.types import TypeEngine, TypeTransformer
from flyte.types._type_engine import TypeTransformerFailedError
class CustomFileTransformer(TypeTransformer[File]):
def __init__(self):
super().__init__(name="CustomFileTransformer", t=File)
def get_literal_type(self, t: Type[File]) -> types_pb2.LiteralType:
"""Get the Flyte literal type for a File type."""
return types_pb2.LiteralType(
blob=types_pb2.BlobType(
format="",
dimensionality=types_pb2.BlobType.BlobDimensionality.SINGLE,
)
)
async def to_literal(
self,
python_val: File,
python_type: Type[File],
expected: types_pb2.LiteralType,
) -> literals_pb2.Literal:
"""Convert a File object to a Flyte literal."""
if not isinstance(python_val, File):
raise TypeTransformerFailedError(f"Expected File object, received \{type(python_val)\}")
return literals_pb2.Literal(
scalar=literals_pb2.Scalar(
blob=literals_pb2.Blob(
metadata=literals_pb2.BlobMetadata(
type=types_pb2.BlobType(
format=python_val.format,
dimensionality=types_pb2.BlobType.BlobDimensionality.SINGLE,
)
),
uri=python_val.path,
)
),
hash=python_val.hash if python_val.hash else None,
)
async def to_python_value(
self,
lv: literals_pb2.Literal,
expected_python_type: Type[File],
) -> File:
"""Convert a Flyte literal to a File object."""
if not lv.scalar.HasField("blob"):
raise TypeTransformerFailedError(f"Expected blob literal, received \{lv\}")
if not lv.scalar.blob.metadata.type.dimensionality == types_pb2.BlobType.BlobDimensionality.SINGLE:
raise TypeTransformerFailedError(
f"Expected single part blob, received \{lv.scalar.blob.metadata.type.dimensionality\}"
)
uri = lv.scalar.blob.uri
filename = Path(uri).name
hash_value = lv.hash if lv.hash else None
return File(path=uri, name=filename, format=lv.scalar.blob.metadata.type.format, hash=hash_value)
def guess_python_type(self, literal_type: types_pb2.LiteralType) -> Type[File]:
"""Guess the Python type from a Flyte literal type."""
if (
literal_type.HasField("blob")
and literal_type.blob.dimensionality == types_pb2.BlobType.BlobDimensionality.SINGLE
):
return File
raise ValueError(f"Transformer \{self\} cannot reverse \{literal_type\}")
Step 2: Register the Transformer
Register your transformer with TypeEngine at module import time:
TypeEngine.register(CustomFileTransformer())
Attempting to register a transformer for a Python type that already has a registered transformer will raise a ValueError to prevent unintended overriding:
# Raises ValueError: Transformer CustomFileTransformer for type \<class 'flyte.io._file.File'> is already registered.
TypeEngine.register(CustomFileTransformer())
Runtime Execution Lifecycle
During workflow and task execution, flyte-sdk runtime routines in _internal/runtime/convert.py invoke TypeEngine helper methods to translate batches of task inputs and outputs.
Converting Inputs to Kwargs
When a task executes, incoming parameters arrive as a LiteralMap. The runtime invokes TypeEngine.literal_map_to_kwargs:
# _internal/runtime/convert.py
native_vals = await TypeEngine.literal_map_to_kwargs(
literals_pb2.LiteralMap(literals=literals),
python_interface.get_input_types(),
)
Internally, literal_map_to_kwargs spawns concurrent asyncio tasks to convert every literal in parallel:
kwargs = \{\}
for i, k in enumerate(lm.literals):
kwargs[k] = asyncio.create_task(
TypeEngine.to_python_value(lm.literals[k], python_interface_inputs[k])
)
await asyncio.gather(*kwargs.values())
Offloaded Literals Unwrapping
When handling large payloads offloaded to remote storage (e.g. S3, GCS), incoming literals contain offloaded_metadata. TypeEngine.to_python_value automatically detects this field and calls TypeEngine.unwrap_offloaded_literal(lv):
if lv.HasField("offloaded_metadata"):
lv = await cls.unwrap_offloaded_literal(lv)
unwrap_offloaded_literal downloads the serialized Protobuf file asynchronously using flyte.storage and loads the unwrapped Literal before passing it to the resolved transformer.
Batched Async Transformations
When transforming collections (ListTransformer) and maps (DictTransformer), flyte-sdk processes items in concurrent batches via _run_coros_in_chunks. The batch concurrency limit is configured by the environment variable _F_TE_MAX_COROS (defaulting to 10):
# Batching coroutines in types/_type_engine.py
lit_list = [TypeEngine.to_literal(x, t, expected.collection_type) for x in python_val]
lit_list = await _run_coros_in_chunks(lit_list, batch_size=_TYPE_ENGINE_COROS_BATCH_SIZE)
Type Constraints and Edge Cases
Restricted Types
tuple, typing.Tuple, and typing.NamedTuple are explicitly registered under TypeEngine._RESTRICTED_TYPES with RestrictedTypeTransformer. Using tuples directly as task input or output types will raise a RestrictedTypeError or AssertionError:
# Tuples cannot be converted directly to individual literal values
TypeEngine.to_literal_checks(python_val=(1, 2), python_type=tuple, expected=...)
# Raises: AssertionError: Tuples are not a supported type for individual values in Flyte...
For multiple task outputs, use typed output syntax, @dataclass, or named attributes instead of raw tuples.
Enum Constraints
EnumTransformer requires all enum members to have string values. Enums with integer or arbitrary values will raise TypeTransformerFailedError("Only EnumTypes with value of string are supported").
String Keys in Dictionaries
For standard Flyte MapType conversion, dictionary keys must be strings (Dict[str, T]). Dictionaries with non-string keys (e.g., Dict[int, str]) bypass MapType and are serialized into binary MessagePack structs.
Cloudpickle Fallback
When a Python type without a registered transformer is passed through TypeEngine, it falls back to FlytePickleTransformer. While this ensures serialization works out of the box, pickled objects:
- Cannot be inspected or visualized in the Flyte UI.
- Require binary compatibility and matching Python/library versions across tasks.
- Incur serialization overhead for large data structures.
Implement and register a custom TypeTransformer for production workflows requiring portable, inspectable IDL types.