tritsystem opened a new issue, #72544:
URL: https://github.com/apache/airflow/issues/72544
### Under which category would you file this issue?
Task SDK
### Apache Airflow version
apache-airflow-core 3.3.1 / apache-airflow-task-sdk 1.3.1 (also confirmed
present on `main` @ 6d4808b4, 2026-09-05)
### What happened and how to reproduce it?
**Issue Description**
`executor_config` inherited from a shared `default_args` dict is ALIASED
(not copied) across every task that doesn't override it — and across every DAG
built from that same `default_args` dict. Reusing one `default_args` module
across multiple DAG files is a common, docs-blessed pattern; mutating
`task_instance.executor_config` at runtime (e.g. to set a Kubernetes pod
override) is also a pattern the Kubernetes-executor docs recommend. Combining
the two silently corrupts sibling tasks and unrelated DAGs.
**Steps to reproduce:**
```python
import copy
from airflow.sdk import DAG
from airflow.providers.standard.operators.python import PythonOperator
default_args = {
"executor_config": {
"KubernetesExecutor": {"image": "base:latest"},
},
}
with DAG(dag_id="dag1", default_args=default_args, schedule=None) as dag1:
task_a = PythonOperator(task_id="task_a", python_callable=lambda: None)
task_b = PythonOperator(task_id="task_b", python_callable=lambda: None)
print(task_a.executor_config is task_b.executor_config) # True -- should be
False
# Mutate only task_a's config (a documented pattern for per-task pod
overrides)
task_a.executor_config["KubernetesExecutor"]["image"] = "custom:v2"
task_a.executor_config["KubernetesExecutor"]["new_key"] = "leaked"
print(task_b.executor_config)
# task_b's config is silently corrupted too -- it never should have changed
# Same aliasing reaches across entirely unrelated DAGs built from the same
default_args:
with DAG(dag_id="dag2", default_args=default_args, schedule=None) as dag2:
task_c = PythonOperator(task_id="task_c", python_callable=lambda: None)
print(task_c.executor_config) # also corrupted -- dag2 never touched dag1's
task
```
Output: `task_a.executor_config is task_b.executor_config` prints `True`,
and after mutating only `task_a`'s config, both `task_b` and `task_c` (in an
entirely different DAG) show the same unwanted mutation.
### What you think should happen instead?
Each task should get an independent copy of `executor_config` inherited from
`default_args`, the same way callback fields (`on_failure_callback` etc.)
already are.
**Root cause:**
- `DAG.default_args` uses `converter=copy.copy` (shallow) —
`task-sdk/src/airflow/sdk/definitions/dag.py`.
- `BaseOperatorMeta.get_merged_defaults` shallow-copies again: `dag_args =
copy.copy(dag.default_args)` — `task-sdk/src/airflow/sdk/bases/operator.py`
(~line 140).
- `BaseOperator.__init__` then does `self.executor_config = executor_config
or {}` (operator.py, line ~1151) with **no per-task copy at all** — unlike
callback fields, which ARE safely normalized via `_collect_from_input()`'s
`list(...)` rebuild (operator.py, line ~457).
- The ORM `TaskInstance` aliases the same dict again: `self.executor_config
= task.executor_config` (`airflow-core/src/airflow/models/taskinstance.py`,
line ~1019) — extending the exposure window.
Every "copy" in the chain is shallow, so the dict object under
`default_args["executor_config"]` ends up literally shared by reference across
every task/DAG that inherits it without overriding it.
**Suggested fix:** apply `copy.deepcopy` (or a per-field normalizer
analogous to `_collect_from_input`) to dict-valued args like `executor_config`
(and similarly-shaped fields, e.g. `params`) when merging `default_args` into
each task, matching the treatment callback fields already get.
Searched issues/PRs for this exact mechanism (`executor_config default_args
shared`, `default_args mutable executor_config`, `executor_config shared
reference`, etc.) — no prior report found; the closest hits (#47702, #53459,
#26568) address key-validation and clear-behavior, not reference sharing.
### Operating System
Windows 11
### Deployment
Virtualenv installation
### Anything else?
Found via a systematic property-based audit (copy/clone independence across
constructor default arguments) run across several widely-used Python libraries.
The repro above was written and independently run twice, with identical results
both times. Happy to send a PR if a maintainer confirms the fix direction
(deepcopy vs. a targeted normalizer).
### Are you willing to submit PR?
- [ ] Yes I am willing to submit a PR!
### Code of Conduct
- [x] I agree to follow this project's Code of Conduct
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]