Textualize/textual · error · DurationParseError

{duration!r} is not a valid duration.

Error message

{duration!r} is not a valid duration.

What it means

_duration_as_seconds parses CSS-style durations (e.g. '400ms', '2s') into seconds. If the string doesn't match a recognized unit pattern, the code falls back to float(duration); when that also fails with ValueError, it raises DurationParseError. (Note: the check is `except ValueError` while float raises TypeError for non-strings in some paths — pass a string or number.)

Source

Thrown at src/textual/_duration.py:42

    Raises:
        DurationParseError: If the argument `duration` is not a valid duration string.
    Returns:
        The duration in seconds.
    """
    match = _match_duration(duration)

    if match:
        value, unit_name = match.groups()
        value = float(value)
        if unit_name == "ms":
            duration_secs = value / 1000
        else:
            duration_secs = value
    else:
        try:
            duration_secs = float(duration)
        except ValueError:
            raise DurationParseError(f"{duration!r} is not a valid duration.") from None

    return duration_secs

View on GitHub (pinned to 06dbeef4bb)

Solutions

  1. Use a supported format: a plain number of seconds ('0.4', 0.4) or a string with s/ms units like '400ms' or '2s'
  2. Validate duration strings at config-load time with a regex or try _duration_as_seconds early
  3. Default missing config values to a valid literal (e.g. '300ms') rather than None
  4. Check for unit typos such as 'sec' instead of 's'

Example fix

# before
process_transition(widget, '0.4sec')  # DurationParseError

# after
process_transition(widget, '0.4s')
Defensive patterns

Strategy: validation

Validate before calling

import re
DURATION_RE = re.compile(r'^[0-9]*\.?[0-9]+(ms|s)?$')

def valid_duration(d) -> bool:
    if isinstance(d, (int, float)):
        return True
    return isinstance(d, str) and bool(DURATION_RE.match(d.strip()))

Type guard

import re
from typing import Any

def is_valid_duration(value: Any) -> bool:
    if isinstance(value, (int, float)):
        return True
    return isinstance(value, str) and bool(re.match(r'^[0-9]*\.?[0-9]+(ms|s)?$', value.strip()))

Try / catch

from textual._duration import DurationParseError
try:
    secs = _duration_as_seconds(raw)
except DurationParseError:
    secs = 0.3  # sensible default

Prevention

When it happens

Trigger: Calling with an unparseable string like 'fast', '400 ms' (depending on supported grammar), '' or '10px'; or passing a non-numeric, non-string type such as None or a list.

Common situations: Loading animation/transition durations from config files or CLI args with typos ('0.5sec' vs '0.5s'), missing values defaulting to None, or wrong units copied from CSS.

Related errors


AI-assisted analysis of Textualize/textual@06dbeef4bb (2026-08-27). Data as JSON: /api/errors/9b6fd7f0c9686615. Report an issue: GitHub.