Skip to content

Fix custom tqdm_class silently broken in non-TTY environments#4056

Open
hanouticelina wants to merge 4 commits intomainfrom
fix/tqdm-class-non-tty
Open

Fix custom tqdm_class silently broken in non-TTY environments#4056
hanouticelina wants to merge 4 commits intomainfrom
fix/tqdm-class-non-tty

Conversation

@hanouticelina
Copy link
Copy Markdown
Contributor

@hanouticelina hanouticelina commented Apr 6, 2026

Fixes #4050.

when passing a custom tqdm_class to hf_hub_download() or snapshot_download(), progress tracking is broken. this is because all tqdm classes are instantiated identically. When a user passes a custom tqdm_class, they take full control of progress bar behavior. The library should not inject HF-specific kwargs (name) or override display logic (disable). the custom class is responsible for its own configuration.
Here is a small reproducible:

from tqdm.auto import tqdm
from huggingface_hub import hf_hub_download


class MyProgress(tqdm):
    def update(self, n=1):
        super().update(n)
        print(f"Progress: {self.n}/{self.total}")


hf_hub_download(
    repo_id="unsloth/gemma-4-26B-A4B-it-GGUF",
    filename="config.json",
    tqdm_class=MyProgress,
    force_download=True,
)

this causes two bugs depending on the environment:

In TTY: disable=None -> isatty() returns True -> tqdm continues __init__ -> hits the unknown kwargs check → TqdmKeyError on name:

python reproducible.py
  ...
  File "huggingface_hub/utils/tqdm.py", line 300, in _get_progress_bar_context
    return (tqdm_class or tqdm)(
    ...
tqdm.std.TqdmKeyError: "Unknown argument(s): {'name': 'huggingface_hub.http_get'}"

In non-TTY: is_tqdm_disabled() returns None. Vanilla tqdm interprets disable=None as "check sys.stderr.isatty()" → False in non-TTY → sets self.disable = Trueupdate() becomes a no-op. No crash, but progress is silently lost:

$ python reproducible.py 2>&1 | cat
Progress: 0/None

Note

Medium Risk
Touches shared progress-bar creation used by downloads; behavior changes could affect progress visibility/TTY handling for some callers, but has no security or data-impacting logic.

Overview
Fixes custom tqdm_class handling so non-HF progress bar implementations aren’t silently disabled in non-TTY environments or crashed by HF-specific kwargs.

Adds _create_progress_bar in utils/tqdm.py and routes _get_progress_bar_context and snapshot_download’s aggregated bytes bar through it, only applying HF disable/name behavior when the class is the Hub’s tqdm subclass (or a subclass). Includes new regression tests covering vanilla/custom tqdm behavior and ensuring HF_HUB_DISABLE_PROGRESS_BARS/name don’t leak into custom classes.

Reviewed by Cursor Bugbot for commit 5d059bf. Bugbot is set up for automated code reviews on this repo. Configure here.

@hanouticelina hanouticelina requested a review from Wauplin April 6, 2026 15:39
pbar.close()


def _create_progress_bar(*, cls: type[old_tqdm], log_level: int, name: str | None = None, **kwargs) -> old_tqdm:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added this private helper to centralize how we initialize the tqdm progress bar. is_tqdm_disabled is no longer needed internally but kept it since it's exposed in huggingface_hub.utils module

@bot-ci-comment
Copy link
Copy Markdown

bot-ci-comment bot commented Apr 6, 2026

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

@tobocop2
Copy link
Copy Markdown

tobocop2 commented Apr 6, 2026

Thanks for the fix @hanouticelina!

There's a related but distinct tqdm bug that this PR's _create_progress_bar would be the ideal place to fix:

tqdm crashes with multiprocessing lock errors when used in threaded contexts such as Textual TUI worker threads, some web server setups, etc.

The error looks like:

ValueError: bad value(s) in fds_to_keep

or

OSError: [Errno 9] Bad file descriptor

This happens because tqdm internally initializes multiprocessing locks during __init__, and in certain threaded environments the file descriptors aren't valid. Since _create_progress_bar is now the single entry point for creating progress bars, this is the perfect place to add a resilient fallback. it would make all downloads robust in threaded contexts without any caller-side changes:

def _create_progress_bar(*, cls, log_level, name=None, **kwargs):
    # ... existing logic ...
    try:
        return cls(disable=disable, name=name, **kwargs)
    except (OSError, ValueError):
        # tqdm multiprocessing lock init can fail in threaded contexts
        # (Textual workers, some web servers). Fall back to disabled bar.
        logger.warning(
            "Progress bar could not be initialized in this environment. "
            "Download will continue without progress reporting. "
            "Set HF_HUB_DISABLE_PROGRESS_BARS=1 to silence this warning."
        )
        return cls(disable=True, **kwargs)

Without this centralized entry point, there's no clean place to catch the error. tqdm creation was previously scattered across multiple call sites. This PR creates the right abstraction to handle it in one place.

Right now users can work around this by setting HF_HUB_DISABLE_PROGRESS_BARS=1, but a library call like hf_hub_download shouldn't crash just because the caller didn't set an env var. The download itself is fine. Tt's only the progress bar initialization that fails. The try/except here ensures downloads work in any runtime context, and callers don't need to defensively configure env vars to avoid crashes from an optional visual feature.

This crash was initial root motivation behind #4051 and I feel it might as well be addressed here.

The non-TTY silencing was a secondary symptom, but the primary crash was tqdm's multiprocessing lock failing in worker threads.

My suggestion is to add this try/except and add a test case before merging. This way a separate fix/pr can be avoided and this is a great defensive measure.

disable = False
else:
disable = None
return cls(disable=disable, name=name, **kwargs) # type: ignore[return-value]
Copy link
Copy Markdown

@tobocop2 tobocop2 Apr 6, 2026

Choose a reason for hiding this comment

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

_create_progress_bar should wrap instantiation in try/except (OSError, ValueError), log a warning, and fall back to a no-op. A progress bar failing to init shouldn't crash the download.

See #4056 (comment)

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.

tqdm_class progress tracking silently broken in non-TTY environments (disable=None auto-detection)

2 participants