python/cpython · error · ValueError

stray %% in format '%s'

Error message

stray %% in format '%s'

What it means

When _strptime compiles the format string, an unknown directive raises a KeyError from TimeRE.__getitem__ whose key encodes the bad directive. If stripping the '\s' escape from that key leaves nothing, the format contained a stray '%' followed by a space (e.g. '% '), and this ValueError is raised.

Source

Thrown at Lib/_strptime.py:567

            time.tzname != locale_time.tzname or
            time.daylight != locale_time.daylight):
            _TimeRE_cache = TimeRE()
            _regex_cache.clear()
            locale_time = _TimeRE_cache.locale_time
        if len(_regex_cache) > _CACHE_MAX_SIZE:
            _regex_cache.clear()
        format_regex = _regex_cache.get(format)
        if not format_regex:
            try:
                format_regex = _TimeRE_cache.compile(format)
            # KeyError raised when a bad format is found; can be specified as
            # \\, in which case it was a stray % but with a space after it
            except KeyError as err:
                bad_directive = err.args[0]
                del err
                bad_directive = bad_directive.replace('\\s', '')
                if not bad_directive:
                    raise ValueError("stray %% in format '%s'" % format) from None
                bad_directive = bad_directive.replace('\\', '', 1)
                raise ValueError("'%s' is a bad directive in format '%s'" %
                                    (bad_directive, format)) from None
            _regex_cache[format] = format_regex
    found = format_regex.match(data_string)
    if not found:
        raise ValueError("time data %r does not match format %r" %
                         (data_string, format))
    if len(data_string) != found.end():
        rest = data_string[found.end():]
        # Specific check for '%:z' directive
        if (
            "colon_z" in found.re.groupindex
            and found.group("colon_z") is not None
            and rest[0] != ":"
        ):
            raise ValueError(
                f"Missing colon in %:z before '{rest}', got '{data_string}'"

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Escape literal percent signs in the format as '%%'
  2. Remove the stray '%' or the trailing space from the format string

Example fix

# before
>>> time.strptime('5 10%', '%d %')
ValueError: stray % in format '%d %'

# after
>>> time.strptime('5 10%', '%d 10%%')
time.struct_time(...)
Defensive patterns

Strategy: validation

Validate before calling

import re

def sanitize_fmt(fmt: str) -> str:
    # escape lone '%' not followed by a known directive
    return re.sub(r'%(?![%aAbBcCxXdeFGHIjmMpSUuVwWxzZ])', '%%', fmt)

Prevention

When it happens

Trigger: A format string containing '% ' (percent followed by whitespace), such as time.strptime(s, '%d % ') — the regex substitution %[-_0^#]*[0-9]*([OE]?[:\]?.?) consumes '%' then treats the space as the directive, producing an empty bad-directive key.

Common situations: User-built or f-string-assembled format strings where a literal '%' was not escaped as '%%'; templates that interpolate a percent into the format.

Related errors


AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14). Data as JSON: /api/errors/19e37ba716f659e3. Report an issue: GitHub.