Core Task Architecture
Flytekit tasks are the fundamental building blocks of execution. While you typically interact with them via the @task decorator, they are implemented through a structured hierarchy of classes that handle everything from type translation to container command generation.
The Task Hierarchy
At the root of the architecture is the Task class in flytekit.core.base_task. This class is a thin wrapper around the Flyte IDL TaskTemplate. It captures metadata and defines the interface but does not have a native Python execution body.
Most tasks you write inherit from PythonTask, which adds support for a Python native Interface. This allows flytekit to translate between Python types (like int, str, or pandas.DataFrame) and Flyte's internal literal system.
The hierarchy typically follows this path:
Task: The base IDL-compatible class.PythonTask: Adds Python type awareness.PythonAutoContainerTask: (Internal) Automatically handles container image resolution and generates thepyflyte-executecommand.PythonFunctionTask: The implementation used for@taskdecorated functions.
Python Function Tasks
When you use the @task decorator, flytekit instantiates a PythonFunctionTask. This class is designed to wrap a standard Python function and make it executable on the Flyte platform.
from flytekit import task
@task(retries=3, cache=True, cache_version="1.0")
def square(n: int) -> int:
return n * n
Internal Execution Flow
When a task runs, it goes through two distinct paths depending on the environment:
- Local Execution: When you call
square(n=5)in a script or test, flytekit invokesTask.local_execute. This method handles local caching (viaLocalTaskCache) and callssandbox_execute, which eventually triggers the user's function. - Remote Execution: On the Flyte cluster, the container starts with a command like
pyflyte-execute. This entry point callsdispatch_execute.
Inside dispatch_execute (found in flytekit.core.base_task.PythonTask), flytekit performs the following steps:
- Input Translation: Converts Flyte
LiteralMapinputs into Python native values using_literal_map_to_python_input. - Pre-execution: Calls
pre_executeto set up the environment (e.g., initializing a Spark session). - User Code Execution: Calls the actual
executemethod (which, inPythonFunctionTask, simply runs the decorated function). - Output Translation: Converts the Python return values back into a Flyte
LiteralMapusing_output_to_literal_map.
Raw Container Tasks
If you need to run code that isn't Python, or you have a pre-built binary in a container, you use ContainerTask. Unlike PythonFunctionTask, which generates its own command, ContainerTask requires you to specify the image, command, and arguments manually.
You can use template strings to map Flyte inputs to command-line arguments:
from flytekit import ContainerTask, kwtypes
calculate_area = ContainerTask(
name="calculate_ellipse_area",
input_data_dir="/var/inputs",
output_data_dir="/var/outputs",
inputs=kwtypes(a=float, b=float),
outputs=kwtypes(area=float),
image="ghcr.io/flyteorg/rawcontainers-python:v2",
command=[
"python",
"calculate-area.py",
"{{.inputs.a}}",
"{{.inputs.b}}",
"/var/outputs",
],
)
In this example, {{.inputs.a}} is a placeholder that Flyte Propeller replaces with the actual input value at runtime. The ContainerTask.execute method (in flytekit.core.container_task) even supports local execution by using docker-py to run the container on your local machine.
Task Metadata and Configuration
The TaskMetadata class in flytekit.core.base_task stores the execution policies for a task. When you pass arguments to the @task decorator, they are encapsulated into this object.
Key attributes include:
retries: Managed byretry_strategy, determining how many times the Flyte platform will attempt to re-run the task on failure.timeout: Adatetime.timedeltathat limits the maximum duration of a single execution.cacheandcache_version: Controls whether Flyte should skip execution if the same inputs have been processed before.interruptible: Indicates if the task can run on lower-cost, preemptible nodes (like AWS Spot Instances).
Task Resolvers
A critical problem in distributed execution is "rehydration": how does a container know which Python function to run? Flytekit solves this using the TaskResolverMixin.
The DefaultTaskResolver (used by most tasks) works by:
- Serialization: At registration time, it records the module name and the function name (e.g.,
my_project.tasks.square). - Loading: At runtime, the
pyflyte-executecommand receives these asloader_args. The resolver usesimportlibto import the module and retrieve the task object by name.
If you have a non-standard way of loading tasks (e.g., loading from a database or a dynamic source), you can implement a custom resolver by inheriting from TaskResolverMixin and overriding load_task and loader_args.
Handling Task Outputs
Flytekit provides a specialized exception, IgnoreOutputs, for scenarios where a task completes but its outputs should not be recorded or passed downstream. This is particularly useful in distributed training where only the rank-0 process should emit the final model or metrics. When IgnoreOutputs is raised, the Flyte platform treats the task as successful but ignores any generated data.