Task authoring and execution
Flyte tasks are the fundamental building blocks of a workflow. In flytekit, tasks are primarily declared using the @task decorator, which transforms a standard Python function into a PythonFunctionTask. This abstraction handles the translation between Python's native types and the Flyte IDL, manages execution metadata, and provides hooks for both local and remote execution.
Declaring Tasks
The @task decorator in flytekit.core.task is the primary entry point for authoring tasks. It supports both bare decoration and keyword arguments for configuration.
from flytekit import task
import typing
@task
def simple_task(x: int) -> str:
return f"Value: {x}"
@task(
cache=True,
cache_version="1.0",
retries=3,
timeout=3600,
container_image="my-custom-image:latest"
)
def configured_task(x: int) -> int:
return x + 1
When you decorate a function, flytekit performs several actions:
- Interface Detection: It uses
transform_function_to_interfaceto inspect your function's type hints and create aTypedInterface. - Metadata Construction: It creates a
TaskMetadataobject to store settings likeretries,cache, andtimeout. - Plugin Dispatch: It uses
TaskPlugins.find_pythontask_pluginto determine which task class to instantiate. For standard functions, this isPythonFunctionTask. Forasyncfunctions, it usesAsyncPythonFunctionTask. - Registration: The task instance is appended to
FlyteEntities.entities, making it discoverable during serialization.
Task Configuration and Metadata
The TaskMetadata class in base_task holds the execution parameters for a task. Some parameters have strict validation rules enforced in __post_init__:
- If
cache=True, acache_versionmust be provided. cache_serializeandcache_ignore_input_varscan only be enabled ifcache=True.timeoutcan be anint(seconds) or adatetime.timedelta.
Task Class Hierarchy
Flytekit uses a layered class hierarchy to separate concerns between the Flyte IDL, Python interfaces, and containerization.
Task: The abstract base class inbase_task. It captures the core Flyte IDLTaskTemplateinformation (name, type, interface).PythonTask: Adds support for Python-native interfaces. It handles the conversion between Flyte literals and Python objects using theTypeEngine.PythonAutoContainerTask: Found inpython_auto_container, this class manages the container image and the default command (pyflyte-execute) used to run the task on a cluster.PythonFunctionTask: The implementation used for@task-decorated functions. It holds a reference to the user'stask_function.
Custom Task Types
While most users use @task, you can create custom task types by subclassing PythonTask or PythonInstanceTask. For example, the Echo task in flytekit.core.task demonstrates a task that simply returns its inputs:
class Echo(PythonTask):
_TASK_TYPE = "echo"
def __init__(self, name: str, inputs: Optional[Dict[str, Type]] = None, **kwargs):
outputs = dict(zip(output_name_generator(len(inputs)), inputs.values()))
super().__init__(
task_type=self._TASK_TYPE,
name=name,
interface=Interface(inputs=inputs, outputs=outputs),
**kwargs,
)
def execute(self, **kwargs) -> Any:
values = list(kwargs.values())
return values[0] if len(values) == 1 else tuple(values)
Execution Lifecycle
Flytekit tasks follow a specific execution flow whether running locally or on a remote cluster.
Local Execution
When you call a task directly in a Python script, Task.__call__ invokes flyte_entity_call_handler. This eventually triggers local_execute, which:
- Translates native Python inputs into Flyte literals using
translate_inputs_to_literals. - Checks the
LocalTaskCacheif caching is enabled. - Calls
sandbox_execute, which sets up a local execution context. - Wraps the results back into
Promiseobjects or aVoidPromise.
Dispatch Execute
The dispatch_execute method in PythonTask is the core execution engine used during runtime (both local and remote). It follows these steps:
pre_execute: Modifies theExecutionParameters(e.g., setting up a Spark session).- Input Translation: Converts the
input_literal_mapinto native Pythonkwargsvia_literal_map_to_python_input. execute: Invokes the actual user function (or the overriddenexecutemethod).post_execute: Allows for cleanup or output modification. If a task raisesIgnoreOutputs, the results are discarded.- Output Translation: Converts Python return values back into a
LiteralMapvia_output_to_literal_map.
Advanced Task Behaviors
Dynamic Tasks
A dynamic task is declared using the @dynamic decorator, which is a shortcut for @task(execution_mode=ExecutionBehavior.DYNAMIC).
When dispatch_execute runs a dynamic task, it calls dynamic_execute. Instead of returning simple values, it invokes compile_into_workflow to produce a DynamicJobSpec. This spec contains a dynamically generated workflow that Flyte Propeller then executes.
Eager Workflows
Eager workflows, declared with @eager, allow for fully dynamic execution where Python constructs like loops and conditionals can control Flyte entity execution.
The EagerAsyncPythonFunctionTask uses a Controller (found in worker_queue) to manage sub-executions. If an eager workflow fails, flytekit uses an EagerFailureHandlerTask to terminate any orphaned sub-executions that might still be running on the cluster.
Task Resolvers
When a task runs on a cluster, the container needs to know how to find and load the Python task object. This is handled by TaskResolverMixin. The default_task_resolver works by:
- Recording the module and function name during serialization (
loader_args). - Importing the module and retrieving the task attribute during execution (
load_task).
Note that the default resolver cannot handle nested or local functions because they are not importable at the module level. If you define a task inside another function, flytekit will raise a ValueError unless you are in a test module (starting with test_).