matplotlib/matplotlib · error · RuntimeError

unmatched closing token {token}

Error message

unmatched closing token {token}

What it means

_balanced_expression (lib/matplotlib/_type1font.py:307) walks the token stream of structural PostScript constructs (e.g. the /Subrs and /CharStrings arrays) keeping a delimiter stack. A closing ']' or '}' arriving when the stack is empty means a close with no matching open, and it raises RuntimeError('unmatched closing token {token}') showing the offending token and its position.

Source

Thrown at lib/matplotlib/_type1font.py:307

        The token that triggered parsing a balanced expression.
    tokens : iterator of _Token
        Following tokens.
    data : bytes
        Underlying data that the token positions point to.

    Returns
    -------
    _BalancedExpression
    """
    delim_stack = []
    token = initial
    while True:
        if token.is_delim():
            if token.raw in ('[', '{'):
                delim_stack.append(token)
            elif token.raw in (']', '}'):
                if not delim_stack:
                    raise RuntimeError(f"unmatched closing token {token}")
                match = delim_stack.pop()
                if match.raw != token.opposite():
                    raise RuntimeError(
                        f"opening token {match} closed by {token}"
                    )
                if not delim_stack:
                    break
            else:
                raise RuntimeError(f'unknown delimiter {token}')
        elif not delim_stack:
            break
        token = next(tokens)
    return _BalancedExpression(
        initial.pos,
        data[initial.pos:token.endpos()].decode('ascii', 'replace')
    )

View on GitHub (pinned to b379c1b69e)

Solutions

  1. Round-trip the font through fontTools.t1Lib or FontForge (they re-emit canonical syntax) and use the result.
  2. Replace the font with a known-good Type-1, or switch the figure's font stack to TrueType/OpenType, which matplotlib handles via FT2Font.
  3. If you generate fonts programmatically, emit balanced delimiters and test-load each output.
  4. Wrap Type1Font construction in try/except RuntimeError and fall back to a default font so export still succeeds.
Defensive patterns

Strategy: try-catch

Validate before calling

def quick_ps_sanity(data: bytes) -> bool:
    head = data[:65536]
    return (data.startswith(b'\x80') or head.startswith(b'%!')) and b'eexec' in head

Type guard

def is_loadable_type1font(path) -> bool:
    from matplotlib.type1font import Type1Font
    try:
        Type1Font(str(path))
        return True
    except (ValueError, RuntimeError):
        return False

Try / catch

try:
    font = Type1Font(path)
except RuntimeError as e:
    if 'unmatched closing token' in str(e):
        font = Type1Font(default_path)
    else:
        raise

Prevention

When it happens

Trigger: Type1Font loading a font whose PostScript dict/array syntax is broken — an extra ']' or '}' before any opening bracket; hand-patched fonts; output of a broken Type-1 generator emitting mismatched delimiters; byte corruption that turns some byte into ']' inside a parsed region.

Common situations: Third-party or converted Type-1 fonts (old vendor .pfb, LaTeX-distro fonts) with sloppy PostScript that other renderers (Ghostscript) tolerate but matplotlib's strict tokenizer rejects; truncated files reassembled incorrectly.

Related errors


AI-assisted analysis of matplotlib/matplotlib@b379c1b69e (2026-08-21). Data as JSON: /api/errors/3992e3f89de560a1. Report an issue: GitHub.