Skip to content

Python 3.9 compatibility#1743

Open
NN--- wants to merge 1 commit into
headroomlabs-ai:mainfrom
NN---:py39-support
Open

Python 3.9 compatibility#1743
NN--- wants to merge 1 commit into
headroomlabs-ai:mainfrom
NN---:py39-support

Conversation

@NN---

@NN--- NN--- commented Jul 3, 2026

Copy link
Copy Markdown

Description

Lower the minimum supported Python from 3.10 to 3.9.

Most of the codebase was already 3.9-clean at the syntax level (thanks to from __future__ import annotations); the real work was runtime semantics. This PR adds a small headroom/_compat.py shim module, gates 3.10-only dependencies behind environment markers, and fixes two genuine 3.9 runtime bugs the test suite exposed.

Closes #1744

Type of Change

  • New feature (non-breaking change that adds functionality)

Changes Made

  • pyproject.toml: requires-python = ">=3.9", 3.9 classifier, ruff target-version = "py39", mypy python_version = "3.9", eval-type-backport dep (lets pydantic evaluate PEP 604 string annotations on 3.9)
  • Gated 3.10-only deps behind python_version >= '3.10' markers: litellm, mcp, torch, tree-sitter(-language-pack), langchain-core/openai/ollama, strands-agents, huggingface-hub pin. All uses were already lazy / ImportError-guarded (same pattern as the existing 3.14 litellm gate, GH [BUG] compression_first_stage always times out (30s) on Python 3.14 + headroom-ai 0.25.0 incompatible with Python 3.14 (litellm dep) #956)
  • Cargo.toml: PyO3 abi3-py310abi3-py39 — one wheel now installs on 3.9–3.14
  • New headroom/_compat.py:
    • aclosing, entry_points_group() — 3.10-only stdlib APIs
    • AsyncLock / AsyncEvent / AsyncSemaphore / AsyncQueue — Python 3.9 asyncio primitives bind get_event_loop() eagerly at construction; these backport 3.10's lazy binding so sync-context construction works and primitives attach to the loop they are awaited in (23 construction sites migrated)
    • DATACLASS_SLOTS@dataclass(**DATACLASS_SLOTS) applies slots=True on 3.10+, no-op on 3.9
  • Fixed real 3.9 bugs: on macOS 3.9 time.monotonic() starts near 0 at process start, so 0.0 "never happened" sentinels wrongly throttled polls/flushes during early process life (subscription/codex_rate_limits.py, memory/traffic_learner.py); bare import tomllib (3.11+) in tests/scripts
  • Mechanical 3.9 fixes via AST codemod: isinstance(x, A | B) → tuples, cast(X | None, …)Optional[…], added missing from __future__ import annotations, removed zip(strict=)
  • CI: new py39-smoke job — installs the abi3 wheel on 3.9, imports headroom, runs the CLI, byte-compiles the installed package
  • Docs: README / installation.mdx / CONTRIBUTING.md now say Python 3.9+

Testing

  • Unit tests pass (pytest)
  • Linting passes (ruff check .)
  • Type checking passes (mypy headroom)
  • Manual testing performed

Test Output

# Python 3.9.25 (uv venv, macOS arm64), full suite:
$ .venv39/bin/python -m pytest tests -q
7 failed, 7139 passed, 720 skipped, 5 warnings in 177.55s (0:02:57)

# Python 3.12 on the same machine and tree (baseline — failures are environmental):
$ .venv312/bin/python -m pytest tests -q
4 failed, 7446 passed, 509 skipped in 282.35s (0:04:42)
# The overlapping failures are machine-specific, not version-specific:
#   test_copilot_auth (real Copilot token in ~/.headroom shadows test stubs),
#   test_release_workflows (cargo not on pytest PATH),
#   test_lossless_mode, backpressure timing tests (flaky under load; pass in isolation)

$ ruff check .        (ruff 0.15.17, CI-pinned)
All checks passed!
$ ruff format --check .
1042 files already formatted
$ mypy headroom --ignore-missing-imports   (mypy 1.20.2, CI-pinned)
Success: no issues found in 407 source files

# Wheel build with abi3-py39:
$ maturin build --out dist
📦 Built wheel for abi3 Python ≥ 3.9 to dist/headroom_ai-0.29.0-cp39-abi3-macosx_11_0_arm64.whl

Real Behavior Proof

  • Environment: macOS 15 (arm64), CPython 3.9.25 via uv python install 3.9, Rust 1.95.0, wheel built with maturin (abi3-py39)
  • Exact command / steps:
    1. uv venv --python 3.9 && uv pip install dist/headroom_ai-0.29.0-cp39-abi3-macosx_11_0_arm64.whl
    2. python -c "import headroom; from headroom._core import hello; print(headroom.__version__, hello())"
    3. Import sweep: pkgutil.walk_packages over the installed package, importing all modules
    4. python -m pytest tests -q on the 3.9 venv (dev+proxy extras)
    5. uv pip compile pyproject.toml --python-version 3.9 --extra <each of proxy,code,ml,memory,vector,relevance,image,dev,all> — all resolve; uv lock regenerated
  • Observed result: wheel installs on 3.9; headroom 0.29.0 + Rust core headroom-core load; 398 modules import, 0 hard failures (4 expected optional-dep skips: litellm/mcp/trafilatura); 7139 tests pass; every extra resolves on both 3.9 and 3.12
  • Not tested: Windows/Linux on 3.9 (CI covers Linux via the new py39-smoke job); GPU/torch paths on 3.9 (torch is gated to 3.10+ by its own wheels); live provider integration tests requiring API keys

Review Readiness

  • I have performed a self-review
  • This PR is ready for human review

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have updated the CHANGELOG.md if applicable (release-please manages the changelog)

Additional Notes

  • On 3.9, extras that depend on 3.10-only packages degrade gracefully: [proxy] installs without mcp (MCP CLI prints an install hint, code is ImportError-guarded), [ml] installs transformers 4.x without torch, [code] no-ops without tree-sitter — matching the existing 3.14/litellm degradation pattern.
  • tests/ monkeypatch targets moved from importlib.metadata.entry_points to the new entry_points_group seam where needed; _compat resolves importlib.metadata.entry_points dynamically so existing global patches keep working on 3.10+.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

PR governance

This PR does not yet satisfy the required template fields:

  • Fill in Real Behavior ProofExact command / steps.

Please update the PR body, or move the PR back to draft while it is still in progress.

@github-actions github-actions Bot added the status: needs author action Pull request body or readiness checklist still needs author updates label Jul 3, 2026
Lower the minimum supported Python from 3.10 to 3.9.

- pyproject: requires-python >=3.9, 3.9 classifier, ruff/mypy 3.9 targets;
  eval-type-backport lets pydantic evaluate PEP 604 string annotations on 3.9
- Gate 3.10-only deps (litellm, mcp, torch, tree-sitter, langchain-*,
  strands-agents) behind python_version >= '3.10' markers; all their uses
  were already lazy/ImportError-guarded
- PyO3: abi3-py310 -> abi3-py39 (wheel now installs on 3.9+)
- Add headroom/_compat.py:
  - aclosing, entry_points_group (3.10-only stdlib APIs)
  - AsyncLock/AsyncEvent/AsyncSemaphore/AsyncQueue: 3.9 asyncio primitives
    bind get_event_loop() eagerly at construction; these backport 3.10's
    lazy binding so sync-context construction works and locks attach to
    the loop they are awaited in
  - DATACLASS_SLOTS: @DataClass(**DATACLASS_SLOTS) applies slots=True on
    3.10+ only
- Fix real 3.9 bugs found by the suite: time.monotonic() starts near 0 on
  macOS 3.9, so 0.0 'never happened' sentinels wrongly throttled the first
  seconds of process life (codex_rate_limits, traffic_learner); bare
  tomllib imports (3.11+) in tests/scripts
- Rewrite runtime PEP 604 unions (isinstance tuples, cast Optional/Union),
  add future annotations imports where missing, drop zip(strict=)
- CI: add py39-smoke job (installs the abi3 wheel on 3.9, imports, CLI)
@NN--- NN--- marked this pull request as ready for review July 3, 2026 12:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: needs author action Pull request body or readiness checklist still needs author updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Python 3.9 compatibility

2 participants