Task authoring and execution
Flyte tasks are the fundamental building blocks of a workflow. In flytekit, tasks are declared using decorators that transform Python functions into specialized task objects. These objects manage the lifecycle of a task, from local execution and testing to serialization and remote execution on a Flyte cluster.
Task Declaration and Configuration
The primary way to author a task is using the @task decorator. This decorator wraps a Python function and instantiates a PythonFunctionTask (or AsyncPythonFunctionTask for coroutines).
from flytekit import task
import datetime
@task(
cache=True,
cache_version="1.0",
retries=3,
timeout=datetime.timedelta(minutes=60),
environment={"MY_ENV_VAR": "value"},
)
def my_task(x: int) -> str:
return str(x)
Task Metadata
Configuration for a task is captured in the TaskMetadata class. This class validates and stores execution parameters:
- Caching: Enabled via
cache=True. You must provide acache_version. You can also usecache_serialize=Trueto ensure identical inputs are processed serially andcache_ignore_input_varsto exclude specific inputs from the cache key. - Retries: Specified as an integer. Flyte will retry the task at least
ntimes on failure. - Timeout: Can be a
datetime.timedeltaor an integer representing seconds. - Interruptible: Indicates if the task can be scheduled on lower-priority, pre-emptible nodes.
Internally, TaskMetadata.__post_init__ enforces strict rules:
- If
cacheisTrue,cache_versionmust be set. cache_serializeandcache_ignore_input_varsrequirecache=True.
Decorator Variants
Flytekit provides specialized decorators for different execution behaviors:
@dynamic: Defined indynamic_workflow_task.pyas a partial of@taskwithExecutionBehavior.DYNAMIC. It allows the task body to act like a workflow, generating new tasks and subworkflows at runtime based on input data.@eager: Constructs anEagerAsyncPythonFunctionTask. Eager workflows allow you to use standard Python control flow (likeifandfor) with Flyte entities by executing them immediately against a backend.
Core Task Abstractions
The task system is built on a hierarchy of classes that separate Flyte IDL concerns from Python-native execution.
The Task Hierarchy
Task: The root base class inbase_task.py. it captures theFlyteIDLspecification, including thetask_type,name, andinterface(as aTypedInterface).PythonTask: Adds a Python-nativeInterfaceto the baseTask. It handles the translation between Flyte literals and Python types using theTypeEngine.PythonFunctionTask: The most common task type. It wraps a user-defined Python function and auto-detects its interface.
Task Resolution
When a task runs on a remote cluster, the container needs to know how to rehydrate the Python task object. This is handled by the TaskResolverMixin. The default resolver identifies the task by its module and function name.
At serialization time, flytekit generates command-line arguments for pyflyte-execute:
pyflyte-execute --resolver flytekit.core.python_auto_container.default_task_resolver \
-- task-module my_module task-name my_task_function
Execution Lifecycle
The dispatch_execute method in PythonTask (and its overrides) defines the lifecycle of a task execution, whether running locally or on a cluster.
1. Pre-execution
The pre_execute method is called before any inputs are converted. This is used to set up environment-specific contexts, such as a SparkSession.
2. Input Translation
Flytekit translates the LiteralMap (Flyte's internal data format) into Python-native keyword arguments using _literal_map_to_python_input. This relies on the TypeEngine to map IDL types to Python types.
3. Execution
The core logic is invoked in the execute method.
- For
PythonFunctionTask, this simply calls the wrapped_task_function(**kwargs). - For
AsyncPythonFunctionTask, it awaits the coroutine. - For
EagerAsyncPythonFunctionTask, it manages aControllerand worker queue to coordinate remote executions.
4. Post-execution and Output Translation
After execute completes, post_execute allows for final cleanup or output modification. Finally, _output_to_literal_map converts the Python return values back into a LiteralMap for Flyte.
5. Decks
If enabled via enable_deck=True, the task generates Flyte Decks. PythonFunctionTask automatically includes SOURCE_CODE and DEPENDENCIES decks by default.
Local Execution and Caching
When you call a task directly in a Python script, it triggers local_execute.
# This triggers local_execute
result = my_task(x=10)
local_execute performs the following:
- Translates native inputs to literals.
- Checks the
LocalTaskCacheifmetadata.cacheis enabled andLocalConfig.auto().cache_enabledis true. - If a cache miss occurs, it calls
sandbox_execute, which eventually runsdispatch_execute. - Wraps the resulting literals back into
Promiseobjects or aVoidPromise.
Implementation Gotchas
- Nested Functions: The default task resolver cannot handle nested or local functions because they cannot be easily imported by the container. Tasks must be accessible at the module level.
- Async and Dynamic:
AsyncPythonFunctionTaskdoes not supportDYNAMICexecution mode. Eager and dynamic behaviors are currently mutually exclusive. - Manual Output Names: If a task returns multiple values, Flyte typically names them
o0,o1, etc. To provide meaningful names, usetyping.NamedTupleas the return type annotation. - IgnoreOutputs: You can raise the
IgnoreOutputsexception within a task to indicate that the generated outputs should be discarded, which is useful in distributed training scenarios.