When would you write `@task` rather than `PythonOperator`, and what does the TaskFlow API actually do for you?
Why they ask this
It checks whether the candidate is current. TaskFlow has been the default style since Airflow 2.0, and a project written entirely in `PythonOperator` with manual `xcom_push` is a tell.
Say this
`@task` for anything Python. It generates the operator, wires dependencies from the function calls, and handles XCom automatically — the return value becomes the XCom and an argument becomes the pull.
The reasoning
With TaskFlow you write `total = transform(extract())` and get three things for free: the tasks, the dependency edges, and the data passing. Without it you write two `PythonOperator`s, an explicit `extract >> transform`, an `xcom_push` or a return, and an `xcom_pull` with the right task id and key in the downstream callable — four places to keep in step instead of one expression.
It is also more testable. A `@task`-decorated function is still a plain function; `my_task.function(arg)` calls the undecorated body, so unit tests do not need an Airflow context at all. And type hints on the parameters are real hints, where `op_kwargs` is an untyped dict.
Classic operators are still right for everything that is not Python. `SQLExecuteQueryOperator`, `KubernetesPodOperator`, `S3ToRedshiftOperator` and the rest of the provider catalogue exist precisely so you do not write that code yourself, and reaching for `@task` with a hand-rolled boto3 call instead of the provider operator is the opposite mistake. The two mix freely in one DAG — a TaskFlow function can be `>>`'d to a classic operator.
The one thing to be careful about is that TaskFlow makes XCom invisible. `transform(extract())` looks like a function call and is actually a database round trip, so the habit of passing a pointer rather than a payload matters *more* in this style, not less — it is easier to accidentally return a DataFrame when it looks like ordinary Python.
The answer most people give
"TaskFlow is just syntax sugar, they are identical." It also derives the dependency graph and the XCom wiring from your code, which are the two things people most often get out of step when writing them by hand.
They’ll ask next
You need to run a query against Snowflake. Would you use @task with a hook, or a provider operator?
