Textualize/rich · error · ValueError

invalid mode {mode!r}

Error message

invalid mode {mode!r}

What it means

Progress.open() normalizes the mode string by sorting its characters and only accepts read modes that reduce to 'r', 'rb' (sorted 'br'), or 'rt'. Any other mode — including write/append modes like 'w', 'a', 'wb+', or unusual orderings like 'b+r' — raises ValueError('invalid mode ...') because this API exists solely to track read progress (downloads).

Source

Thrown at rich/progress.py:1346

            mode (str): The mode to use to open the file. Only supports "r", "rb" or "rt".
            buffering (int): The buffering strategy to use, see :func:`io.open`.
            encoding (str, optional): The encoding to use when reading in text mode, see :func:`io.open`.
            errors (str, optional): The error handling strategy for decoding errors, see :func:`io.open`.
            newline (str, optional): The strategy for handling newlines in text mode, see :func:`io.open`.
            total (int, optional): Total number of bytes to read. If none given, os.stat(path).st_size is used.
            task_id (TaskID): Task to track. Default is new task.
            description (str, optional): Description of task, if new task is created.

        Returns:
            BinaryIO: A readable file-like object in binary mode.

        Raises:
            ValueError: When an invalid mode is given.
        """
        # normalize the mode (always rb, rt)
        _mode = "".join(sorted(mode, reverse=False))
        if _mode not in ("br", "rt", "r"):
            raise ValueError(f"invalid mode {mode!r}")

        # patch buffering to provide the same behaviour as the builtin `open`
        line_buffering = buffering == 1
        if _mode == "br" and buffering == 1:
            warnings.warn(
                "line buffering (buffering=1) isn't supported in binary mode, the default buffer size will be used",
                RuntimeWarning,
            )
            buffering = -1
        elif _mode in ("rt", "r"):
            if buffering == 0:
                raise ValueError("can't have unbuffered text I/O")
            elif buffering == 1:
                buffering = -1

        # attempt to get the total with `os.stat`
        if total is None:
            total = stat(file).st_size

View on GitHub (pinned to 9d8f9a372c)

Solutions

  1. Use a read mode: progress.open(path, 'rb') for binary or 'r'/'rt' for text.
  2. For writing, use the builtin open() and update the progress task manually with progress.update(task_id, completed=f.tell()).
  3. Double-check for typos/extra characters in the mode string (e.g. 'b+r' instead of 'rb').

Example fix

# before
with progress.open('out.bin', 'wb', total=n) as f:  # ValueError: invalid mode 'wb'
    f.write(data)

# after
with open('out.bin', 'wb') as f:
    f.write(data)
    progress.update(task_id, completed=f.tell())
Defensive patterns

Strategy: validation

Validate before calling

VALID = ('r', 'rb', 'rt')
mode = ''.join(sorted(mode))
assert mode in VALID, f'Progress.open supports read modes only, got {mode!r}'

Try / catch

try:
    f = progress.open(path, mode)
except ValueError as e:
    if 'invalid mode' in str(e):
        f = progress.open(path, 'rb')  # normalize to binary read
    else:
        raise

Prevention

When it happens

Trigger: progress.open('file.bin', 'wb'); progress.open('f.txt', 'w'); any mode containing w/a/x/+ characters, or a mode whose sorted form isn't in ('br','rt','r') such as 'b+r'.

Common situations: Copy-pasting an existing open(path, mode) call into progress.open() while keeping a write mode; assuming Progress.open mirrors the builtin open() semantics; trying to log/capture output into a progress-tracked file.

Related errors


AI-assisted analysis of Textualize/rich@9d8f9a372c (2026-08-15). Data as JSON: /api/errors/17a51fe54c037762. Report an issue: GitHub.