python/cpython · error · ValueError

Non-digit values in fraction

Error message

Non-digit values in fraction

What it means

Once a '.' or ',' fraction separator is seen, every remaining character must be an ASCII digit. Any letter, sign, or whitespace in the fraction (e.g. '12:30:00.5Z0', '12:30:00.+01:00') raises ValueError('Non-digit values in fraction'). Note this also fires when a timezone suffix was not split off correctly, because _parse_hh_mm_ss_ff receives the naive time portion only.

Source

Thrown at Lib/_pydatetime.py:430

        if comp == 0:
            has_sep = next_char == ':'

        if not next_char or comp >= 2:
            break

        if has_sep and next_char != ':':
            raise ValueError("Invalid time separator: %c" % next_char)

        pos += has_sep

    if pos < len_str:
        if tstr[pos] not in '.,':
            raise ValueError("Invalid microsecond separator")
        else:
            pos += 1
            if not all(map(_is_ascii_digit, tstr[pos:])):
                raise ValueError("Non-digit values in fraction")

            len_remainder = len_str - pos

            if len_remainder >= 6:
                to_parse = 6
            else:
                to_parse = len_remainder

            time_comps[3] = int(tstr[pos:(pos+to_parse)])
            if to_parse < 6:
                time_comps[3] *= _FRACTION_CORRECTION[to_parse-1]

    return time_comps

def _parse_isoformat_time(tstr):
    # Format supported is HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]
    len_str = len(tstr)
    if len_str < 2:

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Uppercase the UTC marker and validate shape: s = s.replace('z', 'Z') then re-check before parsing (on Pythons that lack full ISO support)
  2. Upgrade to Python 3.11+, where fromisoformat handles most valid ISO-8601 including 'Z'
  3. Use dateutil.parser.isoparse for permissive but standards-correct ISO input

Example fix

// before
dt = datetime.fromisoformat('2021-01-01T12:30:00.000z')  # may hit fraction/garbage errors on <3.11

# after
dt = datetime.fromisoformat('2021-01-01T12:30:00.000Z')  # 3.11+
# or: from dateutil.parser import isoparse; dt = isoparse(raw)
Defensive patterns

Strategy: validation

Validate before calling

def norm_utc_marker(s: str) -> str:
    return s[:-1] + 'Z' if s.endswith('z') else s

Type guard

def fraction_is_digits(t: str) -> bool:
    import re
    m = re.search(r'[.,](\d*)$', t)
    return m is None or m.group(1).isdigit()

Try / catch

try:
    dt = datetime.fromisoformat(s)
except ValueError:
    from dateutil.parser import isoparse
    dt = isoparse(s)  # tolerant fallback for legacy feeds

Prevention

When it happens

Trigger: datetime.fromisoformat('2021-01-01T12:30:00.000+01:00') works, but a malformed sign placement like '12:30:00.5+1:00' can reach the digit check on some paths; fraction fields containing spaces or symbols from bad exports; tz_pos detection missing a non-standard suffix character (e.g. lowercase 'z' in older versions).

Common situations: Lowercase 'z' UTC markers on Python < 3.11 fromisoformat; mixed precision where fraction and offset run together after string surgery; fixed-width record parsing whose offsets drifted.

Related errors


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