misleading abstraction
The Error Named a Python Class, Not the Bug: ansible-core 2.19's Data Tagging Rewrite
TL;DR: ansible-playbook started failing with errors naming internal Python
types most people have never heard of — _AnsibleTaggedList, type-combination
failures between a dict and something that isn't quite a dict. The actual
trigger was a one-line YAML formatting mistake in a single file. Fixing every place
that error seemed to point at first would have meant refactoring a large chunk of
the codebase for the wrong reason.
The symptom
[ERROR]: unexpected parameter type in action: <class '...AnsibleTaggedList'>
and, in a different play:
failed to combine variables, expected dicts but got a 'dict' and a '_AnsibleTaggedList'
Neither error names a variable, a file, or a task — just an internal Python class that doesn't appear anywhere in this build's actual code.
Investigation
Research confirmed ansible-core 2.19 introduced a large internal rewrite —
"Data Tagging" — to how variable type and origin metadata gets tracked through
templating and module argument passing. Described by the wider community as the
largest change to ansible-core's internals since collections were introduced, with
multiple official and third-party collections still shipping compatibility fixes
for it well after release. That context mattered: it meant the error text pointing
at an obscure internal type wasn't a fluke, it was a symptom of a real, actively
evolving compatibility gap.
The first instinct — pin back to ansible-core 2.18 — hit its own, completely
unrelated wall: the original install had gone through apt/PPA, which made core
dependencies like resolvelib dpkg-owned rather than pip-owned. pip refused to
uninstall packages it had no installation record for, blocking the version change
before it could even be tried.
Rather than resolve the packaging conflict first, the actual playbook error was traced to its specific trigger before touching anything about the version: a YAML file containing a list that mixed plain strings and dictionary mappings as sibling items — a pattern that had worked fine before, and that 2.19's new Data Tagging logic appears to handle inconsistently during variable-merge operations. On closer inspection, the specific file actually causing the immediate error turned out to have a genuine, separate formatting mistake, adjacent to but distinct from that broader mixed-type-list pattern.
Root cause
Two distinct things stacked together: a real, one-line formatting error in a single
group_vars file, and a broader, still-evolving compatibility gap in
ansible-core 2.19+'s handling of mixed-type variable merging that made diagnosing
the first issue harder than it needed to be — because the error pointed at the
symptom (an internal Python type) rather than the cause (malformed YAML).
Fix
The formatting error itself was a direct, one-line correction. The broader version
question was a separate, deliberate decision rather than something to keep fighting
mid-debug: stay on ansible-core 2.18.x rather than chase a still-shifting 2.19+
compatibility target for now. Three real options existed for actually managing that
version going forward, given the apt/pip ownership conflict:
- Purge apt's copy entirely and go pip-only
- Manage the version strictly through apt, accepting no control over timing
- Isolate a pip-managed
ansible-coreinside a Python virtualenv, keeping the two installation methods from ever touching each other's dependencies
Went with the venv. Purging apt risked cascading removal of system packages other
tools might depend on (python3-yaml, python3-jinja2 among them); managing
strictly through apt meant no control over version timing when a future
compatibility issue like this one shows up again. A venv isolates the pip-managed
Ansible completely, so neither installation path can conflict with the other going
forward.
What this demonstrates
When a new, large-scope error appears right after a tooling change, the instinct to fix everything the error text seems to be describing is worth resisting until the specific trigger is confirmed. Here, that would have meant auditing and potentially rewriting every mixed-type list across the codebase, when the real fault was a single line in one file. Diagnosing the specific trigger before choosing a general fix avoided a large, unnecessary refactor. The version-management decision alongside it is a reminder that not every resolution is a bug fix — sometimes it's picking the isolation boundary that's easiest to reverse if it doesn't work out, rather than the option that looks cleanest on paper but is harder to undo.