Skip to content

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

Merged
hanouticelina merged 5 commits intomainfrom
fix/tqdm-class-non-tty
Apr 7, 2026
Merged

Fix custom tqdm_class silently broken in non-TTY environments#4056
hanouticelina merged 5 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

⚠️ BREAKING CHANGE: custom tqdm_class instances no longer receive the name kwarg or disable value, the custom class is fully responsible for its own configuration.


Note

Low Risk
Low risk: change is isolated to progress bar instantiation logic and adds regression tests; main behavior change only affects callers providing a custom tqdm_class, especially in non-TTY environments.

Overview
Fixes progress reporting when callers pass a custom tqdm_class by centralizing progress-bar creation in _create_progress_bar and only applying Hugging Face–specific kwargs (name) and disable/TTY logic for the HF tqdm subclass.

Updates both _get_progress_bar_context and snapshot_download to use this helper, and adds regression tests to ensure group-based disabling still works for HF bars while vanilla/custom tqdm classes are neither force-disabled in non-TTY nor passed the unsupported name kwarg.

Reviewed by Cursor Bugbot for commit d5d3eba. 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
Contributor

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 this defensive measure. 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)

Right now users can work around this by setting HF_HUB_DISABLE_PROGRESS_BARS=1, or calling the disable progress bar function but a library call like hf_hub_download shouldn't crash just because the caller didn't set an env var or remember to call a function. The download itself is fine. It'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 their code to avoid crashes from this library.

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
Contributor

@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)

Copy link
Copy Markdown
Contributor

@Wauplin Wauplin left a comment

Choose a reason for hiding this comment

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

I'm fine with this fix 👍

Approving but could you still address comment before merging?

Also could you mention explicitly in the PR description that this is a breaking change so we pick it up for the next release notes (it's a breaking change since we change the default behavior of custom tqdm classes)

Comment thread src/huggingface_hub/utils/tqdm.py Outdated
@hanouticelina
Copy link
Copy Markdown
Contributor Author

@Wauplin updated the PR description and addressed your comment, thanks!

@hanouticelina hanouticelina merged commit 0fa8edc into main Apr 7, 2026
15 of 21 checks passed
@hanouticelina hanouticelina deleted the fix/tqdm-class-non-tty branch April 7, 2026 17:40
@tobocop2
Copy link
Copy Markdown
Contributor

tobocop2 commented Apr 7, 2026

@Wauplin @hanouticelina this is the second time my concerns were ignored or dismissed silently.

My feedback on this PR was completely ignored which is troublesome. If my concerns are wrong or invalid, then they should be acknowledged and resolved and not ignored. If I'm in the wrong place, I should be told this as well. I would have never put in the effort had I known it wouldn't even be read.

I put a large amounts of effort into providing exhaustive amounts of information, reproducing bugs, creating detailed descriptions, creating a fix and they just got completely dismissed with zero acknowledgement which isn't great. Nobody should be ignored like this. A simple acknowledgement would have been sufficient. A culture of ignoring is just not cool, especially if the concerns are warranted and valid. All I asked for was to roll in a defensive measure here, and what got merged in will crash specifically in the contexts I mentioned. If my approach isn't good, then say so and be direct. My ego isn't big and I'm always prepared to be wrong about something. Could simply suggest opening a separate PR or you can tell me that my approach is bad, but the silence is why I'm raising an alarm.

It started here: #4051

my bug reports, granularity concerns, all dismissed and not acknowledged. I understand my implementation not being merged in, but all the concerns in this PR were just completely either not read or just not appreciated at all. My implementation was deemed "too complicated" but it also was a completely backwards compatible additive not breaking change and that was my philosophy. The code here is a deliberate breaking change and something I was trying to avoid introducing as an outsider to the org. I understand it not being merged in due to the complexity, but the surrounding concerns of the crash and the progress granularity being ignored is what is unsettling. A change request could have also been an option and I would have happily made the change

Most importantly, the crash concern got completely ignored in this pr.

I expect similar communication patterns with my followup

issues:
#4058
#4066

PRs:
#4059
#4065

If someone puts significant effort into something, the general rule that we all should stand by is acknowledging their concerns. In that previous PR, i identified two serious bugs, and the response was along the lines of "this is too complicated and not worth it" and I get it but there were other issues flagged in this PR that got dismissed completely which is what concerns me. This is poor communication. Suggesting a different implementation and acknowledging the issues I flagged is what I was hoping for.

You can say something like "Create a separate issue", or "this concern will be addressed in a separate PR" or "thanks for the report, but we'll be doing this differently" or even in the case of this PR, just an emoji reaction would be sufficient, but instead it got completely ignored and now I feel less compelled to report any bugs in the future. Ignoring is not acceptable and an improvement in communication is needed.

I normally do not create noise like this, but I obviously do not like being ignored and I apologize for the wall of text. My goal is to help make this library as good as it can be and work well for everyone. When serious concerns are raised and ignored, it's just baffling to me. I really appreciate this library and I can't help but call out this behavior in an effort to improve communication with those outside of the org. Like I said before, being ignored makes me not want to report these bugs anymore, so I really hope this communication issue gets addressed.

@tobocop2
Copy link
Copy Markdown
Contributor

ferris

@Wauplin
Copy link
Copy Markdown
Contributor

Wauplin commented Apr 23, 2026

Hey @tobocop2 , thanks for your help. I've just reviewed #4059 and #4065. I understand the frustration but please don't be too pushy.

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)

4 participants