Skip to content

Regression of #305 in v0.10.0: disable_numparse=True is ignored when maxcolwidths is set #428

@liquidsec

Description

@liquidsec

Disclaimer: The following was generated with AI. Bug was originally discovered on BBOT via our CI dependabot: blacklanternsecurity/bbot#3045

This is a regression. The same bug was previously reported as #305 ("Can not tabulate row with a field that looks like a Python Bool using maxcolwidths"), fixed by PR #362 ("Fix handling 'True'/'False' bool str and None"), merged on 2025-07-23. The buggy code came back in a later merge and is now present on master and in the v0.10.0 release.

Symptom

When disable_numparse=True is combined with maxcolwidths, cells that look number-ish but cannot actually be parsed (e.g. '80,443', a comma-separated port list) crash with ValueError: invalid literal for int() with base 10. The same input works fine without maxcolwidthsdisable_numparse=True is honored on the non-wrap code path, but ignored on the wrap path.

Repro (v0.10.0, also present on master)

from tabulate import tabulate
tabulate(
    [['ports', 'str', 'comma-separated port list', '80,443']],
    ['name', 'type', 'desc', 'default'],
    tablefmt='grid',
    disable_numparse=True,
    maxcolwidths=40,
)

Traceback:

File ".../tabulate/__init__.py", line 1705, in _wrap_text_to_colwidths
    else str(_type(cell, numparse)(cell))
ValueError: invalid literal for int() with base 10: '80,443'

Omit maxcolwidths and disable_numparse=True is honored and the call succeeds.

Root cause

In _wrap_text_to_colwidths, the per-cell numparse flag is correctly computed (the traceback's locals show numparses = [False, False, False, False] for this call), and it is correctly honored in the first branch:

if _isnumber(cell) and numparse:
    new_row.append(cell)
    continue

But a few lines later it is passed positionally into _type:

else str(_type(cell, numparse)(cell))

_type's signature is:

def _type(string, has_invisible=True, numparse=True):

so the False lands in the has_invisible slot and numparse stays at its default True. Result: _type('80,443') returns int, then int('80,443') raises.

This is what PR #362's commit d29909b already called out:

"calling _type is incorrect here; the subsequent lines need a str. … This regressed in e8e3091…"

How it came back

  1. 886e2ed (Sep 2022) — introduced the buggy _type(cell, numparse)(cell) line.
  2. d29909b / PR Fix handling "True"/"False" bool str and None #362 (Mar 2025) — replaced the whole conditional with casted_cell = str(cell). Issue Can not tabulate row with a field that looks like a Python Bool using maxcolwidths #305 closed as completed on 2025-07-23.
  3. A subsequent merge brought the old buggy version back (now wrapped in str(...) and reformatted by ruff/black, but functionally the same _type(cell, numparse) call). Master and v0.10.0 both ship the regressed code.

Impact

Anyone passing disable_numparse=True together with maxcolwidths and a string cell that doesn't satisfy _isnumber but is recognized as numeric by _type's default numparse=True (e.g. '80,443', 'True', 'False') will crash. We hit this in BBOT's CLI, which renders module-options tables; '80,443' is a default value for several modules.

Environment

  • tabulate 0.10.0 (also reproduced building from current master)
  • Python 3.10 – 3.14 (reproduced on all)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions