Skip to main content

Unions, Enums, and Optional Types

In this SDK, branching logic and optionality are handled through standard Python type hints. The UnionTransformer and EnumTransformer classes in flyte.types._type_engine manage the conversion between Python's flexible type system and Flyte's structured IDL.

Optional Types

The most common use of branching logic is the Optional type. In Python, Optional[T] is a shorthand for Union[T, None]. The UnionTransformer specifically detects this pattern using the is_optional_type static method:

# src/flyte/types/_type_engine.py

@staticmethod
def is_optional_type(t: Type[Any]) -> bool:
return _is_union_type(t) and type(None) in get_args(t)

When a task input is marked as Optional, Flyte allows the value to be missing or explicitly set to None. This is frequently used for parameters with default values.

Example: Optional Task Inputs

In examples/basics/union_type_simple.py, a task handles an optional string:

@env.task
def process_union_input(input_val: Union[str, None]) -> str:
if input_val is None:
return "Input was None!"
return f"Input was a string: {input_val}"

Union Types

UnionTransformer allows a single input or output to accept multiple distinct types, such as Union[int, str] or Union[MyDataclass, str].

Structural Matching and Ambiguity

When converting a Python value to a Flyte literal, the UnionTransformer iterates through all possible subtypes defined in the Union. It attempts to use the TypeEngine to find a transformer for each subtype.

A critical implementation detail is how the SDK handles ambiguity. If a value could validly represent more than one type in the Union (for example, two different Dataclasses that have the same field names and types), the UnionTransformer will raise a TypeError.

# src/flyte/types/_type_engine.py (UnionTransformer.to_literal)

if is_ambiguous:
raise TypeError(
f"Ambiguous choice of variant for union type.\n"
f"Potential types: {potential_types}\n"
"These types are structurally the same, because it's attributes have the same names and associated types."
)

To avoid this, ensure that the types within a Union are structurally distinct or use unique field names in your Dataclasses and Pydantic models.

Enums

The EnumTransformer enables the use of enum.Enum for categorical inputs. This provides a way to restrict task inputs to a predefined set of valid strings.

String-Valued Enums

Flyte primarily supports string-valued enums. The EnumTransformer enforces this during the conversion process:

# src/flyte/types/_type_engine.py (EnumTransformer.get_literal_type)

values = [v.value for v in t] # type: ignore
if not isinstance(values[0], str):
raise TypeTransformerFailedError("Only EnumTypes with name of value are supported")

Names vs. Values

By default, the SDK serializes enums using their member names, not their values. For example, if you have Color.RED = "red-value", Flyte will store the string "RED" in the underlying literal.

# src/flyte/types/_type_engine.py (EnumTransformer.to_literal)

return Literal(scalar=Scalar(primitive=Primitive(string_value=python_val.name)))

Example: Defining Enums

As seen in examples/basics/enums.py, you can define enums using standard Python syntax:

class Color(str, enum.Enum):
RED = "red-value"
GREEN = "green-value"
BLUE = "blue-value"

@env.task
async def standalone_enum_echo(color: Color) -> str:
return f"color={color.name}({color.value})"

Literal Types

The SDK also supports typing.Literal for cases where you want to restrict a value to a specific set of constants without defining a full Enum class. The EnumTransformer handles these by treating them as a special LITERAL_ENUM.

In examples/basics/all_types.py, typing.Literal is used for simple categorical flags:

Intensity = Literal["low", "medium", "high"]

@env.task
async def process_custom_types(my_literal: Intensity) -> Dict[str, str]:
return {"literal": f"Intensity: {my_literal}"}

When using typing.Literal, the EnumTransformer uses the literal values themselves rather than member names, as there are no member names associated with a typing.Literal.

Integration with Complex Types

Unions and Enums are recursively supported within more complex structures like Pydantic models and Dataclasses. When the PydanticTransformer or DataclassTransformer encounters a field typed as a Union or Enum, it delegates the serialization and deserialization to the UnionTransformer and EnumTransformer respectively.

This allows for deeply nested structures with branching logic:

class ShirtOrder(BaseModel):
color: Color # Enum
size: Size # Enum
quantity: int

class FullOrder(BaseModel):
items: list[ShirtOrder]
notes: Optional[str] = None # Optional/Union

In this example, the TypeEngine coordinates multiple transformers to ensure every field is correctly mapped to the Flyte IDL.