Skip to content

feat: replace python-dateutil with stdlib datetime.fromisoformat#1429

Open
splch wants to merge 4 commits into
openapi-generators:mainfrom
splch:drop-python-dateutil
Open

feat: replace python-dateutil with stdlib datetime.fromisoformat#1429
splch wants to merge 4 commits into
openapi-generators:mainfrom
splch:drop-python-dateutil

Conversation

@splch
Copy link
Copy Markdown

@splch splch commented Apr 24, 2026

Summary

Drop the python-dateutil dependency from both the generator and all generated client packages. Date/datetime parsing now uses Python's built-in datetime.fromisoformat() instead of dateutil.parser.isoparse().

This removes one runtime dependency from every generated client, reducing install size and eliminating a dependency that is unmaintained upstream and being deprecated by Fedora 45 (no releases since March 2024).

Changes

Generator source (openapi_python_client/parser/properties/):

  • datetime.py: Validate defaults with datetime.datetime.fromisoformat() instead of isoparse(). Normalize Z to +00:00 at generation time so the emitted Python code is clean.
  • date.py: Validate defaults with datetime.date.fromisoformat() instead of isoparse().date().
  • Both: Remove "from dateutil.parser import isoparse" from the import set returned by get_imports().

Jinja templates (openapi_python_client/templates/property_templates/):

  • datetime_property.py.jinja: isoparse(x) -> datetime.datetime.fromisoformat(x.replace("Z", "+00:00"))
  • date_property.py.jinja: isoparse(x).date() -> datetime.date.fromisoformat(x)

Dependency removal (all 4 metadata formats):

  • pyproject_uv.toml.jinja, pyproject_poetry.toml.jinja, pyproject_pdm.toml.jinja, setup.py.jinja: Remove python-dateutil from generated dependencies.
  • pyproject.toml (generator): Remove python-dateutil from runtime deps and types-python-dateutil from dev deps.
  • integration-tests/pyproject.toml: Remove both as well.

Tests & golden records: All regenerated. Unit tests pass (283 passed, 4 skipped).

Python 3.10 compatibility

datetime.fromisoformat() gained full ISO 8601 support (including the Z suffix) in Python 3.11. On Python 3.10, the Z suffix raises a ValueError:

# Python 3.10
>>> datetime.datetime.fromisoformat("2024-01-15T10:30:00Z")
ValueError: Invalid isoformat string: '2024-01-15T10:30:00Z'

>>> datetime.datetime.fromisoformat("2024-01-15T10:30:00+00:00")
datetime.datetime(2024, 1, 15, 10, 30, tzinfo=datetime.timezone.utc)  # works
# Python 3.11+
>>> datetime.datetime.fromisoformat("2024-01-15T10:30:00Z")
datetime.datetime(2024, 1, 15, 10, 30, tzinfo=datetime.timezone.utc)  # works natively

The generated datetime parsing code uses .replace("Z", "+00:00") to normalize Z to an explicit UTC offset before calling fromisoformat(), which works on both 3.10 and 3.11+. This is a no-op on strings without Z. Date parsing does not need this since date strings have no timezone component.

Default values in OpenAPI specs are normalized at generation time (Z -> +00:00), so the emitted default expressions are clean datetime.datetime.fromisoformat("...") calls.

Drop the python-dateutil dependency from both the generator and all
generated client code. Date/datetime parsing now uses the stdlib:

- datetime fields: datetime.datetime.fromisoformat(v.replace("Z", "+00:00"))
- date fields: datetime.date.fromisoformat(v)

The .replace("Z", "+00:00") call is needed because Python 3.10's
fromisoformat() does not accept the Z timezone suffix (added in 3.11).
It is a no-op on strings that do not contain Z.

Default values in OpenAPI specs are normalized at generation time
(Z replaced with +00:00), so the generated default expressions are
clean datetime.datetime.fromisoformat("...") calls without the
replace.

This removes one runtime dependency from every generated client
package, reducing install size and eliminating a dependency that is
in maintenance-only mode upstream.
if value is None or isinstance(value, Value):
return value
if isinstance(value, str):
normalized = value.replace("Z", "+00:00")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should either put this behind a version check or leave a comment to remove it when we drop 3.10 support (so when I search for 3.10-specific things in a few months I'll find it)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good :) i added a comment with a TODO to make it easy to find

@@ -1,5 +1,5 @@
{% macro construct_function(property, source) %}
isoparse({{ source }})
datetime.datetime.fromisoformat({{ source }}.replace("Z", "+00:00"))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leave a comment on this one too.

Comment thread pdm.minimal.lock Outdated
[[package]]
name = "coverage"
version = "7.11.0"
version = "7.13.5"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this file was not updated with -S direct_minimal_versions. The goal of it is to make sure everything works with lower bounds.

Same with other minimal lock file

splch added 3 commits May 27, 2026 14:17
Make the .replace("Z", "+00:00") workaround discoverable by both
`rg TODO` and `rg "3.10|py3.10"` so it can be cleanly removed
when the project drops Python 3.10 support.
Brings in 8 commits since the PR opened, notably:
  - typer constraint bump to <0.27
  - uv_build 0.11 template update
  - mypy v2 cast cleanup in generated output

Conflict resolution:
  - pdm.lock, integration-tests/pdm.lock: regenerated via 'pdm lock'
    against the merged pyproject.toml.
  - Golden records: regenerated via 'pdm run regen' to absorb the
    mypy-cast cleanup; the dateutil -> fromisoformat change from this
    branch is preserved.

Verified locally with ruff check, ruff format --check, mypy, and the
unit-test suite (283 passed, 4 skipped).
The minimal lockfiles were previously regenerated without
'-S direct_minimal_versions', so direct dependencies were locked
to highest-compatible versions instead of lowest. That defeats the
point of the test_min_deps CI job, which exists to verify the
declared lower bounds in pyproject.toml still work.

Re-locked both pdm.minimal.lock files using:
    pdm lock -S direct_minimal_versions -L pdm.minimal.lock

Strategy marker confirmed as
["direct_minimal_versions", "inherit_metadata"] in the metadata
of both files. python-dateutil is absent (this branch's main change).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants