Skip to content

Coerce ClassLabel.names to plain str to fix invalid YAML in README.md - #8454

Open
RudrenduPaul wants to merge 1 commit into
huggingface:mainfrom
RudrenduPaul:fix-6919-classlabel-names-yaml
Open

RudrenduPaul wants to merge 1 commit into
huggingface:mainfrom
RudrenduPaul:fix-6919-classlabel-names-yaml

Conversation

@RudrenduPaul

Copy link
Copy Markdown

Coerce ClassLabel.names to plain str to fix invalid YAML in README.md

Fixes #6919

Problem

push_to_hub can fail with:

ValueError: Invalid metadata in README.md.
- Invalid YAML in README.md: unknown tag !<tag:yaml.org,2002:python/tuple> (50:11)

This happens whenever a ClassLabel's names list contains elements that
are not exactly str — most commonly numpy.str_, which shows up whenever
names is built from numpy.unique(...) or read out of a pandas column
(a very common pattern for NER/token-classification labels, exactly as
described in the issue).

Root cause

ClassLabel.__post_init__ already normalizes the internal _int2str
mapping with str(name), but it left the public names list untouched.
When the dataset card YAML metadata is generated
(Features._to_yaml_list -> yaml.dump), PyYAML's default Dumper
selects a representer by exact type match, not isinstance. Since
numpy.str_ is a subclass of str but not str itself, it falls
through to the generic object representer, which serializes it with
unsafe !!python/object/apply:numpy... tags (including base64-encoded
binary state). The resulting README.md is technically written, but it
can no longer be parsed back with the strict yaml.safe_load used to
validate dataset card metadata, hence the "unknown tag" error.

Fix

Coerce every element of ClassLabel.names to a plain str in
__post_init__, mirroring what already happens for _int2str. This
guarantees only native Python strings ever reach the YAML dumper,
regardless of where names originally came from.

Testing

  • Added a regression test
    (test_class_label_names_are_coerced_to_str in
    tests/features/test_features.py) that builds a ClassLabel from
    numpy.str_ values, dumps its YAML representation, and asserts it
    round-trips through yaml.safe_load — this reproduces the exact
    failure from the issue and fails without the fix.
  • Ran the full tests/features/test_features.py, tests/test_info.py,
    and tests/test_metadata_util.py suites locally: 226 passed, 3
    skipped, no regressions.
  • Verified with a minimal repro matching the issue's steps (labels
    derived via numpy.unique, cast to
    Sequence(ClassLabel(names=list(labels))), then dumping the resulting
    DatasetInfo to YAML) — the dump now round-trips through
    yaml.safe_load cleanly instead of raising ConstructorError.

Scope

Only src/datasets/features/features.py (the fix) and
tests/features/test_features.py (the regression test) are touched. No
unrelated code or docs were changed.


Note: this PR was prepared with AI assistance (Claude Code) under my
direction — I reviewed the root-cause analysis, the fix, and the test
before submitting.

numpy.str_ (and similar numpy-backed) entries slip into ClassLabel.names
when built from numpy.unique(...) or a pandas column. PyYAML's default
Dumper only matches representers by exact type, so these fall back to
unsafe !!python/object/apply tags when the dataset card metadata is
dumped, producing a README.md that fails to parse back with
yaml.safe_load.

Fixes huggingface#6919

@shashvat-singham shashvat-singham left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reproduced the motivating problem on main and it is worse than the PR description suggests — worth pasting, because the failure is invisible until someone tries to read the card back:

f = Features({"label": ClassLabel(names=list(np.array(["negative", "positive"])))})
yaml.dump(f._to_yaml_list())
        '0': !!python/object/apply:numpy._core.multiarray.scalar
        - !!python/object/apply:numpy.dtype
          args: [U8, false, true]
          state: !!python/tuple [3, <, null, null, null, 32, 4, 8]
        - !!binary |
          cAAAAG8AAABzAAAAaQAAAHQAAABpAAAAdgAAAGUAAAA=
safe_load FAILED: ConstructorError could not determine a constructor for the tag
'tag:yaml.org,2002:python/object/apply:numpy._core.multiarray.scalar'

So the label names are emitted as base64-encoded numpy scalars and the resulting README.md metadata block cannot be parsed by yaml.safe_load at all. With this branch it dumps as plain negative / positive and round-trips. The fix is right.

Worth adding to the PR description why this slips through every existing guard: numpy.str_ is a subclass of str, so isinstance(name, str) passes everywhere and nothing upstream flags it. PyYAML dispatches on exact type(), not isinstance, which is the only place the difference shows up. That is the whole bug in one sentence and it is currently not stated anywhere in the PR.

Two behaviour changes the coercion also makes, neither obviously wrong but neither mentioned:

Non-string names are now stringified. ClassLabel(names=[0, 1, 2]) gives [0, 1, 2] on main and ['0', '1', '2'] here. str2int(0) already failed on both (Values 0 should be a string or an Iterable), so the ints were not usable as labels anyway and this arguably repairs them — but int2str(0) now returns '0' where it returned 0 before. If that is intended it is worth a line in the docstring, since ClassLabel does not currently say names must be strings.

Tuples become lists. ClassLabel(names=("a", "b")) keeps a tuple on main and becomes ['a', 'b'] here, because the comprehension rebuilds it. That looks like a genuine improvement — names is typed as list[str] and two ClassLabels built from a tuple and a list now compare equal where they did not before — just noting it is a real change beyond the stated scope.

One small thing on the test: assert type(names[0]) is not str as a sanity check will silently stop testing anything if a future numpy returns real str there. assert type(names[0]) is np.str_ states the precondition you actually depend on.

Tested on Windows 11 / Python 3.11.9 / numpy 2.4.6 / PyYAML, main vs pr-8454.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Invalid YAML in README.md: unknown tag !<tag:yaml.org,2002:python/tuple>

2 participants