Skip to content

Commit 9f3fcdc

Browse files
migurskiclaude
andauthored
Prepare DSPy for Python 3.15 (#10521)
* build: raise requires-python cap to <3.16 for Python 3.15 Python 3.15 is being released, but every published dspy declares requires-python >=3.10,<3.15, so pip/uv refuse to install dspy on 3.15 at resolution time. This raises the upper bound to <3.16 so the package can be installed and exercised on 3.15. Metadata-only. It does not itself assert full 3.15 support; it opens the door to validating and releasing it. Happy to help test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: add Python 3.15 to the test and build matrices Adds 3.15 to the test, extra_test and build_package matrices so the suite and the wheel build are exercised on 3.15. allow-prereleases lets the 3.15 leg acquire an interpreter before GA; it is a no-op for the versions that already have stable builds and once 3.15 is released. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: allow pyo3 forward-compat so the 3.15 leg builds sdist deps The 3.15 matrix leg fails at `uv sync`: fastuuid (and other pyo3 extensions) have no cp315 wheel yet, so uv builds them from sdist and pyo3 refuses to build against a Python newer than its max supported. PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 is pyo3's documented escape hatch: build against the stable ABI, run forward-compatibly on 3.15. It is a no-op on versions with a prebuilt wheel. Drop once deps ship cp315. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: keep 3.15 red without a hack; document the upstream unlock Remove the pyo3 abi3 forward-compat env: it cannot make the 3.15 leg green anyway, because pydantic-core subclasses the native PyTzInfo type that abi3 forbids. Instead leave the 3.15 leg failing and document why and what fixes it: the leg goes green with no change here once pydantic ships a release bundling pydantic-core >= 2.48 (first with cp315 wheels). Today the newest pydantic (2.13.5) pins pydantic-core 2.46.5, which has no cp315 wheel, so uv builds it from source and pyo3 cannot compile it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * build: regenerate uv.lock for the raised requires-python pyproject raised requires-python to <3.16; regenerate the lock so it matches (its requires-python and resolution-markers now cover 3.15). No dependency versions change. Fixes the pyproject/lock mismatch that a frozen install would reject. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: make the 3.15 leg an allowed failure and cite the pydantic unlock The 3.15 legs fail until their native deps ship cp315 wheels, and the matrix defaults to fail-fast, so a red 3.15 leg cancels the 3.10-3.14 legs and reddens main. Add continue-on-error for the 3.15 matrix value in test, extra_test and build_package so 3.15 still runs and reports but neither fails the workflow nor cancels the other legs. Update the note with the concrete unlock: pydantic 2.14.0 pins a cp315-capable pydantic-core -- already proven by the published 2.14.0b2 pre-release (pydantic-core 2.49.0, which ships cp315 wheels) -- so once 2.14.0 releases the leg installs from wheels and greens with no change here. Also name fastuuid as the first build wall. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * build: unblock the 3.15 leg now that pydantic 2.14.0 is released pydantic 2.14.0 pins pydantic-core 2.50.0, which ships cp315 wheels. Bump both in uv.lock, and bump pyarrow to 26.0.0 for the 3.15 resolve because 25.0.1 has no cp315 wheel. fastuuid (a litellm dependency) still has no cp315 wheel, and its pyo3 0.26 rejects a Python past 3.14. Set PYO3_USE_ABI3_FORWARD_COMPATIBILITY on the 3.15 legs of test, extra_test and build_package so it builds against the stable ABI. pydantic-core no longer needs this path because it now installs from a wheel. The dev extra and the LiteLLM proxy test skip matched Python 3.14 exactly, so 3.15 got neither litellm[proxy] nor the skip and 10 proxy tests errored. Widen both to 3.14 and later. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * build: install on Python 3.15 without litellm or gepa litellm (since 1.93.0) and gepa 0.1.4 declare requires-python <3.15. pip honours that, so `pip install dspy` failed on 3.15: it rejected gepa, then fell back to litellm 1.83.7, whose pinned fastuuid and pydantic-core have no cp315 wheels and cannot build under PyO3 0.26. uv ignores the cap but still had to build fastuuid from source, which needs Rust plus PYO3_USE_ABI3_FORWARD_COMPATIBILITY. Mark litellm and gepa `python_version < '3.15'` in the core dependencies. The bundled lm15 engine serves LM calls without litellm, and the existing `require()` helper reports an install hint for the litellm-only paths. Declare jsonschema, which dspy/adapters/types/tool.py imports but which only arrived through litellm. Replace dspy.GEPA with a stand-in that raises ImportError when gepa is absent, so `import dspy` still works. Keep gepa in the dev extra so the 3.15 CI legs still test GEPA. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: keep the GEPA stand-in a Teleprompter Code that wraps dspy.GEPA reads GEPA.__init__ and GEPA.compile at import time; cmpnd's auto_instrument does. The bare stand-in class had no compile, so on Python 3.15 without gepa that code raised AttributeError before the user ever constructed a GEPA. Subclass Teleprompter so the stand-in has the same interface, and still raise ImportError on construction. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ea89611 commit 9f3fcdc

5 files changed

Lines changed: 448 additions & 141 deletions

File tree

‎.github/workflows/run_tests.yml‎

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,18 +70,31 @@ jobs:
7070
test:
7171
name: Run Tests
7272
runs-on: ubuntu-latest
73+
# 3.15 is an allowed failure (see the matrix note). Tolerating it stops a
74+
# failure from failing the workflow and, under the default fail-fast, from
75+
# cancelling the 3.10-3.14 legs.
76+
continue-on-error: ${{ matrix.python-version == '3.15' }}
77+
env:
78+
# fastuuid has no cp315 wheel and its pyo3 rejects 3.15; build it against
79+
# the stable ABI on the 3.15 leg. pyo3 reads only the value "1".
80+
PYO3_USE_ABI3_FORWARD_COMPATIBILITY: ${{ matrix.python-version == '3.15' && '1' || '0' }}
7381
permissions:
7482
contents: read
7583
strategy:
7684
matrix:
77-
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
85+
# 3.15 is an allowed failure (continue-on-error above) while Python 3.15
86+
# is a release candidate. Its deps install from cp315 wheels except
87+
# fastuuid (a litellm dep), whose pyo3 rejects a Python past 3.14; the
88+
# job env builds it against the stable ABI on the 3.15 leg only.
89+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
7890
steps:
7991
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
8092
with:
8193
persist-credentials: false
8294
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
8395
with:
8496
python-version: ${{ matrix.python-version }}
97+
allow-prereleases: true
8598
- name: Install uv with caching
8699
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
87100
with:
@@ -103,18 +116,27 @@ jobs:
103116
extra_test:
104117
name: Run Extra and Deno Tests
105118
runs-on: ubuntu-latest
119+
# 3.15 is an allowed failure (see the note in the `test` job); tolerated so
120+
# it does not fail the workflow or, under fail-fast, cancel the 3.10-3.14 legs.
121+
continue-on-error: ${{ matrix.python-version == '3.15' }}
122+
env:
123+
# fastuuid has no cp315 wheel and its pyo3 rejects 3.15; build it against
124+
# the stable ABI on the 3.15 leg. pyo3 reads only the value "1".
125+
PYO3_USE_ABI3_FORWARD_COMPATIBILITY: ${{ matrix.python-version == '3.15' && '1' || '0' }}
106126
permissions:
107127
contents: read
108128
strategy:
109129
matrix:
110-
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
130+
# 3.15 is an allowed failure (see the note in the `test` job above).
131+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
111132
steps:
112133
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
113134
with:
114135
persist-credentials: false
115136
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
116137
with:
117138
python-version: ${{ matrix.python-version }}
139+
allow-prereleases: true
118140
- name: Install uv with caching
119141
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
120142
with:
@@ -256,18 +278,27 @@ jobs:
256278
build_package:
257279
name: Build Package
258280
runs-on: ubuntu-latest
281+
# 3.15 is an allowed failure (see the note in the `test` job); tolerated so
282+
# it does not fail the workflow or, under fail-fast, cancel the 3.10-3.14 legs.
283+
continue-on-error: ${{ matrix.python-version == '3.15' }}
284+
env:
285+
# fastuuid has no cp315 wheel and its pyo3 rejects 3.15; build it against
286+
# the stable ABI on the 3.15 leg. pyo3 reads only the value "1".
287+
PYO3_USE_ABI3_FORWARD_COMPATIBILITY: ${{ matrix.python-version == '3.15' && '1' || '0' }}
259288
permissions:
260289
contents: read
261290
strategy:
262291
matrix:
263-
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
292+
# 3.15 is an allowed failure (see the note in the `test` job above).
293+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
264294
steps:
265295
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
266296
with:
267297
persist-credentials: false
268298
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
269299
with:
270300
python-version: ${{ matrix.python-version }}
301+
allow-prereleases: true
271302
- name: Install uv with caching
272303
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
273304
with:

‎dspy/teleprompt/__init__.py‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,24 @@
1414
from dspy.teleprompt.teleprompt_optuna import BootstrapFewShotWithOptuna
1515
from dspy.teleprompt.vanilla import LabeledFewShot
1616

17-
from .gepa.gepa import GEPA
17+
try:
18+
from .gepa.gepa import GEPA
19+
except ModuleNotFoundError as error:
20+
if error.name != "gepa":
21+
raise
22+
23+
class GEPA(Teleprompter):
24+
"""Stand-in for GEPA when the gepa package is not installed.
25+
26+
DSPy installs gepa only on Python < 3.15, because gepa declares requires-python <3.15. The stand-in keeps
27+
GEPA's Teleprompter interface, so code that inspects or wraps GEPA.__init__ and GEPA.compile still imports.
28+
"""
29+
30+
def __init__(self, *args, **kwargs):
31+
raise ImportError(
32+
"gepa is required to use dspy.GEPA. It is not installed by default on Python 3.15 and later "
33+
"because gepa declares requires-python <3.15."
34+
)
1835

1936
__all__ = [
2037
"AvatarOptimizer",

‎pyproject.toml‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ description = "DSPy"
1313
readme = "README.md"
1414
authors = [{ name = "Omar Khattab", email = "okhattab@stanford.edu" }]
1515
license = {file = "LICENSE"}
16-
requires-python = ">=3.10, <3.15"
16+
requires-python = ">=3.10, <3.16"
1717
classifiers = [
1818
"Development Status :: 3 - Alpha",
1919
"Intended Audience :: Science/Research",
@@ -28,14 +28,17 @@ dependencies = [
2828
"requests>=2.31.0",
2929
"pydantic>=2.11.0",
3030
"pyyaml>=6.0.1",
31-
"litellm>=1.65.8",
31+
# litellm and gepa declare requires-python <3.15, so pip cannot install them on 3.15.
32+
# The bundled lm15 engine serves LM calls without litellm; `dspy[litellm]` opts back in.
33+
"litellm>=1.65.8; python_version < '3.15'",
34+
"jsonschema>=4.0.0",
3235
"diskcache>=5.6.0",
3336
"json-repair>=0.54.2",
3437
"tenacity>=8.2.3", # undeclared runtime dep of litellm, needed for exponential_backoff_retry
3538
"anyio",
3639
"cachetools>=5.5.0",
3740
"cloudpickle>=3.1.2",
38-
"gepa[dspy]==0.1.4",
41+
"gepa[dspy]==0.1.4; python_version < '3.15'",
3942
]
4043

4144
[project.optional-dependencies]
@@ -61,8 +64,9 @@ dev = [
6164
"numpy>=1.26.0",
6265
# LiteLLM proxy imports an internal FastAPI helper removed in 0.140.7.
6366
"fastapi<0.140.7; sys_platform != 'win32' and python_version < '3.14'",
64-
"litellm>=1.65.8; sys_platform == 'win32' or python_version == '3.14'",
65-
"litellm[proxy]>=1.65.8; sys_platform != 'win32' and python_version < '3.14'", # Remove 3.14 condition once uvloop supports
67+
"litellm>=1.65.8; sys_platform == 'win32' or python_version >= '3.14'",
68+
"litellm[proxy]>=1.65.8; sys_platform != 'win32' and python_version < '3.14'", # Remove the 3.14+ condition once uvloop supports it
69+
"gepa[dspy]==0.1.4",
6670
]
6771
test_extras = [
6872
"mcp; python_version >= '3.10'",

‎tests/test_utils/server/__init__.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ def _litellm_test_server() -> Iterator[tuple[str, str]]:
2727
Start one LiteLLM test server per pytest worker and tear it down when the
2828
session completes.
2929
"""
30-
if sys.version_info[:2] == (3, 14):
31-
pytest.skip("Litellm proxy server is not supported on Python 3.14.")
30+
if sys.version_info[:2] >= (3, 14):
31+
pytest.skip("Litellm proxy server is not supported on Python 3.14+.")
3232
with tempfile.TemporaryDirectory() as server_log_dir_path:
3333
# Create a server log file used to store request logs
3434
server_log_file_path = os.path.join(server_log_dir_path, "request_logs.jsonl")

0 commit comments

Comments
 (0)