GitHub user yudin-s added a comment to the discussion: Serialization issues 
with external_python decorator

After your update, I would look less at the body of the failing 
`external_python` task and more at what is being passed into it.

For `@task.external_python`, Airflow still serializes the callable arguments 
before it starts the external interpreter. In the current operator code, the 
args/kwargs are written with the selected serializer as roughly:

```python
{"args": self.op_args, "kwargs": resolved_kwargs}
```

The docs also call out two separate constraints:

- the function body is cut out into a temporary file, so imports must be inside 
the function and globals should not be relied on;
- `op_args`, `op_kwargs`, and return values still need to be serializable 
between the Airflow process and the external Python process.

That matches the clue you added: the function named in the `PicklingError` is 
not part of the task that fails. It can still be inside an upstream task's 
return value, a config object, a connection wrapper, a partially-applied 
argument, or something captured in an expanded/partial mapped argument. Airflow 
may be failing while serializing the arguments for the external task, not while 
running the function body.

I would narrow it this way:

1. Do not pass Airflow `Connection` objects, hooks, callables, modules, or rich 
config objects through XCom into an external task. Pass only primitives: 
strings, numbers, booleans, lists, and dicts of those.

2. For connections, pass a `conn_id` or a plain dict of exactly the fields you 
need:

```python
@task
def get_config():
    return {
        "source_conn_id": "my_source",
        "target_conn_id": "my_target",
    }
```

Then resolve/import inside the external task:

```python
@task.external_python(python="/path/to/venv/bin/python")
def load_data(config, rows):
    from airflow.hooks.base import BaseHook

    source = BaseHook.get_connection(config["source_conn_id"])
    # or import your own DB client and use primitive host/user/schema fields
```

If the external environment does not have the same Airflow version installed, 
avoid `BaseHook` inside it and pass primitive connection fields instead.

3. Check `.partial(...)` and `.expand(...)` inputs specifically. A 
non-serializable object in a partial kwarg can fail later in a downstream 
mapped external task, which makes the error look unrelated.

4. The `Can't pickle local object my_dag.<locals>.get_config` error usually 
means a function object itself is being passed or captured somewhere. Make sure 
you are passing `get_config()` / `XComArg` results, not the `get_config` 
function object, and that any returned config dict does not contain callables.

5. If you truly need to pass a more complex but still normal Python object, try 
`serializer="cloudpickle"` and install `cloudpickle` where Airflow expects it. 
But I would not use that to pass live Airflow objects; it is better to make the 
task boundary plain-data-only.

So the practical rule is: keep all imports inside the external function, but 
also make every upstream XCom, mapped argument, and partial kwarg crossing into 
`external_python` plain data. The error naming `get_connection` / `get_config` 
is probably pointing at one of those values being serialized, not necessarily 
code executed by the failing task.

If this points you to the bad mapped/partial value, please mark it as answered 
so other `external_python` serialization cases can find the argument-boundary 
diagnosis.


GitHub link: 
https://github.com/apache/airflow/discussions/71281#discussioncomment-17960128

----
This is an automatically sent email for [email protected].
To unsubscribe, please send an email to: [email protected]

Reply via email to