Task authoring and execution
Flyte tasks are the fundamental building blocks of a workflow. In flytekit, tasks are primarily authored using the @task decorator, which transforms a standard Python function into a PythonFunctionTask. This abstraction handles the translation between Python types and the Flyte IDL, manages execution metadata, and provides hooks for local and remote execution.
Declaring Tasks
The most common way to define a task is by decorating a typed Python function. flytekit uses the function's type annotations to automatically derive the task's interface (inputs and outputs).
from flytekit import task
import typing
@task
def my_task(x: int, y: typing.Dict[str, str]) -> str:
return f"{x} - {y}"
Internally, the @task decorator in flytekit/core/task.py performs several steps:
- Metadata Construction: It creates a
TaskMetadataobject to store configuration like retries, timeouts, and caching. - Interface Discovery: It calls
transform_function_to_interfaceto map Python types to Flyte's type system. - Task Instantiation: It selects the appropriate task class (e.g.,
PythonFunctionTaskfor standard functions orAsyncPythonFunctionTaskforasyncfunctions) and instantiates it. - Wrapper Update: It uses
functools.update_wrapperso the resulting task object retains the original function's docstrings and metadata.
Task Configuration
You can configure task behavior by passing arguments to the @task decorator. These settings are stored in TaskMetadata and PythonTask attributes.
from flytekit import task, Resources, Cache
@task(
retries=3,
timeout=3600,
requests=Resources(cpu="2", mem="500Mi"),
cache=True,
cache_version="1.0",
environment={"MY_ENV_VAR": "value"}
)
def configured_task(x: int) -> int:
return x + 1
- Caching: When
cache=Trueis set, flytekit requires acache_version. TheTask.local_executemethod checks theLocalTaskCachebefore execution. If a hit is found, it returns the cachedLiteralMapinstead of running the user code. - Resources:
requestsandlimitsallow you to specify the hardware requirements for the task container on the Flyte cluster. - Timeouts: The
timeoutparameter can be anint(seconds) ordatetime.timedelta.TaskMetadata.__post_init__ensures this is normalized to atimedelta.
Core Task Abstractions
The task hierarchy in flytekit provides a layered approach to execution:
Task: The base class (flytekit/core/base_task.py) that captures the Flyte IDLTaskTemplate. It defines the corelocal_executelogic and thedispatch_executeinterface.PythonTask: A subclass that adds support for Python-native interfaces. It handles the asynchronous conversion of native outputs back to Flyte literals in_output_to_literal_map.PythonAutoContainerTask: Used for tasks that run within a container. It integrates withTaskResolverMixinto determine how the task should be rehydrated in a remote environment.PythonFunctionTask: The standard implementation for tasks defined by a Python function. It stores thetask_functionand invokes it during theexecutephase.
Task Resolvers
When a task is executed on a Flyte cluster, the container needs to know which task to run. This is handled by a TaskResolverMixin. The DefaultTaskResolver serializes the task's module and name into the container's command-line arguments:
pyflyte-execute --resolver flytekit.core.python_auto_container.default_task_resolver \
-- task-module my_project.tasks task-name my_task
At runtime, the resolver uses importlib.import_module to load the task object back into memory. Because of this, tasks must generally be defined at the module level to be importable.
Execution Modes
Flytekit supports different execution behaviors via the ExecutionBehavior enum in PythonFunctionTask.
Default Execution
In DEFAULT mode, the task function is executed normally. For local execution, Task.local_execute translates inputs into literals, calls dispatch_execute, and then converts the results back into Promise objects.
Dynamic Tasks
Dynamic tasks (declared with @dynamic) allow you to generate a workflow structure at runtime based on inputs.
from flytekit import dynamic, task
@task
def t1(a: int) -> int:
return a + 1
@dynamic
def my_dynamic_task(n: int) -> typing.List[int]:
return [t1(a=i) for i in range(n)]
When a dynamic task runs on the Flyte platform, PythonFunctionTask.compile_into_workflow is called. It produces a DynamicJobSpec containing the generated nodes and task templates, which Flyte Propeller then executes as a sub-workflow.
Eager Workflows
Eager workflows (@eager) allow for fully imperative execution where every task call is immediately dispatched to the Flyte backend (or run locally).
from flytekit import eager, task
@eager
async def my_eager_workflow(x: int) -> int:
# This task is executed immediately, and the result is awaited
first_res = await add_one(x=x)
return await double(x=first_res)
Eager tasks use an internal Controller to manage the execution queue. If an eager workflow fails, flytekit can use an EagerFailureHandlerTask to clean up any orphaned executions that were started by the parent.
Local Testing and Mocking
Flytekit provides utilities for testing tasks without a full Flyte backend. You can call a task directly like a function, or use task_mock to replace its execution logic during a test.
from flytekit.testing import task_mock
@task
def t1(i: int) -> int:
return i + 1
def test_my_logic():
with task_mock(t1) as m:
m.side_effect = lambda i: i * 10
assert t1(i=2) == 20
The task_mock utility temporarily replaces the execute method of the PythonTask instance, ensuring that the mock is only active within the context manager.