# Faust Python 3.12+ Compatibility Plan

> **Version**: 1.0  
> **Date**: February 18, 2026  
> **Current Faust Version**: 1.16.7  
> **Current Python**: 3.9  
> **Minimum Python**: 3.10+  
> **Target Python**: 3.12+  
> **Codebase Size**: ~157 source files (`faust/`), ~180 test files (`t/`), 3 Cython extensions

---

## Table of Contents

1. [Executive Summary](#1-executive-summary)
2. [Codebase Component Map](#2-codebase-component-map)
3. [Issue Inventory](#3-issue-inventory)
4. [Phase 0 — Prerequisites & Dependency Audit](#4-phase-0--prerequisites--dependency-audit)
5. [Phase 1 — mode-streaming Upgrade (Critical Blocker)](#5-phase-1--mode-streaming-upgrade-critical-blocker)
6. [Phase 2 — asyncio loop= Parameter Removal](#6-phase-2--asyncio-loop-parameter-removal)
7. [Phase 3 — Typing & Model System Fixes](#7-phase-3--typing--model-system-fixes)
8. [Phase 4 — Cython Extension Rebuild](#8-phase-4--cython-extension-rebuild)
9. [Phase 5 — Deprecation Warnings & Cleanup](#9-phase-5--deprecation-warnings--cleanup)
10. [Phase 6 — Test Infrastructure & Validation](#10-phase-6--test-infrastructure--validation)
11. [Phase 7 — Dependency Version Bumps](#11-phase-7--dependency-version-bumps)
12. [Risk Assessment & Decision Points](#12-risk-assessment--decision-points)
13. [Verification Checklist](#13-verification-checklist)

---

## 1. Executive Summary

Faust has **three categories** of Python 3.12 incompatibility:

| Category | Severity | Scope | Owner |
|----------|----------|-------|-------|
| `asyncio` `loop=` parameter removed (Python 3.10) | **BREAKING** | 105+ source occurrences, 49+ test occurrences, 25+ files | Faust + mode-streaming |
| `mode-streaming` 0.3.5 incompatible | **BLOCKING** | 194 import sites, entire Service lifecycle | Upstream / fork |
| Typing internals + NamedTuple changes | **HIGH** | `faust/models/typing.py`, mode-streaming `objects.py` | Faust + mode-streaming |

**The single biggest blocker is `mode-streaming`.** Even if all Faust code is fixed, mode-streaming 0.3.5 will crash on Python 3.12 because `asyncio.ensure_future(loop=)`, `asyncio.Future(loop=)`, and `asyncio.Event(loop=)` were removed in Python 3.10.

**Recommended approach**: Bump mode-streaming to `>=0.4.0`, verify at runtime, and patch remaining `loop=` issues in both mode-streaming and Faust.

---

## 2. Codebase Component Map

The Faust codebase divides into these logical components. Each must be analyzed and fixed independently:

### Component A: Core Application (`faust/app/`)
- `faust/app/base.py` (~1944 lines) — Main App class, event loop management, service orchestration
- `faust/app/_attached.py` — Attached resource management
- `faust/app/router.py` — Request routing
- **Issues**: 19+ `loop=` occurrences in `base.py` alone

### Component B: Streams & Channels (`faust/streams.py`, `faust/channels.py`, `faust/topics.py`)
- `faust/streams.py` (~700 lines) — Stream processing core
- `faust/channels.py` — Channel abstraction
- `faust/topics.py` — Kafka topic management
- **Issues**: `loop=` in constructors and `asyncio.Event(loop=)` calls

### Component C: Agents (`faust/agents/`)
- `faust/agents/agent.py` — Agent implementation
- `faust/agents/replies.py` — Reply/promise system (`ReplyPromise(asyncio.Future)`)
- **Issues**: `loop=` threading, `asyncio.Future` subclass with `loop=`, `asyncio.Queue(loop=)`

### Component D: Transport Layer (`faust/transport/`)
- `faust/transport/base.py` — Base transport classes
- `faust/transport/consumer.py` (~1400 lines) — Consumer implementation
- `faust/transport/producer.py` — Producer implementation
- `faust/transport/conductor.py` — Topic conductor
- `faust/transport/drivers/aiokafka.py` (~1800 lines) — **19 `loop=` occurrences** — heaviest file
- `faust/transport/drivers/confluent.py` — Confluent driver (optional)
- `faust/transport/_cython/conductor.pyx` — Cython accelerated conductor
- **Issues**: Most concentrated `loop=` usage in the codebase

### Component E: Tables & State (`faust/tables/`)
- `faust/tables/base.py` — Table base class
- `faust/tables/manager.py` — Table manager
- `faust/tables/recovery.py` — Recovery system (`asyncio.Event(loop=)`)
- `faust/tables/sets.py` — Set tables
- **Issues**: `loop=` in manager, recovery events

### Component F: Models & Serialization (`faust/models/`)
- `faust/models/base.py` — Model metaclass and base
- `faust/models/record.py` — Record model (calls `mode.utils.objects.annotations()`)
- `faust/models/typing.py` — Typing utilities (**`_field_types` on NamedTuple** — broken in 3.12)
- `faust/models/fields.py` — Field descriptors
- `faust/models/tags.py` — Tag types (Personal, Sensitive)
- **Issues**: `NamedTuple._field_types` removed in 3.12; depends on mode typing introspection

### Component G: Worker & CLI (`faust/worker.py`, `faust/cli/`)
- `faust/worker.py` — Worker process (extends `mode.Worker`)
- `faust/cli/base.py` — CLI framework (`asyncio.get_event_loop()`, `loop=`)
- `faust/cli/worker.py` — Worker CLI command
- **Issues**: Event loop lifecycle management, `loop=` threading

### Component H: Web Layer (`faust/web/`)
- `faust/web/base.py` — Web base
- `faust/web/drivers/aiohttp.py` — aiohttp web driver (`loop=`)
- **Issues**: `loop=` passed to aiohttp server

### Component I: Utilities (`faust/utils/`)
- `faust/utils/codegen.py` — Dynamic code generation (exec-based)
- `faust/utils/functional.py` — Functional utilities
- `faust/utils/platforms.py` — Platform detection
- `faust/utils/venusian.py` — Plugin system
- `faust/utils/json.py` — JSON utilities
- `faust/utils/cron.py` — Cron utilities
- **Issues**: Minimal direct issues; mostly depends on mode utilities

### Component J: Type Interfaces (`faust/types/`)
- 24 files defining abstract types/interfaces
- `faust/types/app.py`, `types/transports.py`, `types/channels.py`, etc.
- **Issues**: `loop` parameter in abstract method signatures (must be updated in sync with implementations)

### Component K: Cython Extensions (`faust/_cython/`)
- `faust/_cython/streams.pyx` — Accelerated stream processing (**has `loop=`**)
- `faust/_cython/windows.pyx` — Accelerated window operations (clean)
- `faust/transport/_cython/conductor.pyx` — Accelerated conductor (clean)
- **Issues**: `streams.pyx` has `sleep(0, loop=self.loop)` — broken; all need recompilation

### Component L: Sensors & Monitoring (`faust/sensors/`)
- Datadog, statsd, prometheus integrations
- **Issues**: Minimal — mostly clean

### Component M: LiveCheck (`faust/livecheck/`)
- `faust/livecheck/case.py` — `datetime.utcnow()` deprecated
- `faust/livecheck/models.py` — `datetime.utcnow()` deprecated
- `faust/livecheck/app.py` — Uses `mode.utils.objects.annotations()`
- **Issues**: `datetime.utcnow()` deprecation warning (not a crash)

---

## 3. Issue Inventory

### 3.1. BREAKING — asyncio `loop=` Parameter (Removed in Python 3.10)

The `loop` parameter was removed from ALL asyncio high-level APIs in Python 3.10:
- `asyncio.sleep(loop=)` → just `asyncio.sleep()`
- `asyncio.Event(loop=)` → just `asyncio.Event()`
- `asyncio.Future(loop=)` → just `asyncio.Future()`
- `asyncio.Queue(loop=)` → just `asyncio.Queue()`
- `asyncio.ensure_future(loop=)` → just `asyncio.ensure_future()`
- `asyncio.gather(loop=)` → just `asyncio.gather()`
- `asyncio.wait(loop=)` → just `asyncio.wait()`
- `asyncio.shield(loop=)` → just `asyncio.shield()`

**Affected files (source — 25+ files, 105+ occurrences):**

| File | Occurrences | Key Patterns |
|------|-------------|--------------|
| `faust/transport/drivers/aiokafka.py` | 19 | `AIOKafkaConsumer(loop=)`, `asyncio.shield(loop=)`, `MessageAccumulator(loop=)` |
| `faust/app/base.py` | 19 | `Service.__init__(loop=)`, `FlowControlEvent(loop=)`, `ThrowableQueue(loop=)` |
| `faust/transport/consumer.py` | 6 | `loop=` param, `MethodQueue(loop=)`, `asyncio.Future(loop=)` |
| `faust/transport/conductor.py` | 4 | `asyncio.Event(loop=)`, `asyncio.Future(loop=)` |
| `faust/transport/drivers/confluent.py` | 6 | `loop=self.loop`, `ProducerProduceFuture(loop=)` |
| `faust/streams.py` | 6 | `Service.__init__(loop=)`, `asyncio.Event(loop=)`, `current_task(loop=)` |
| `faust/channels.py` | 3 | `loop` param in `__init__` |
| `faust/topics.py` | 2 | `loop` param in `__init__` |
| `faust/agents/agent.py` | 4 | `loop=self.loop` passed to channels/topics |
| `faust/agents/replies.py` | 2 | `asyncio.Queue(loop=)`, `self._loop` (Future internal) |
| `faust/tables/recovery.py` | 3 | `Event(loop=self.loop)` |
| `faust/tables/manager.py` | 3 | `Event(loop=self.loop)` |
| `faust/tables/base.py` | 1 | `loop=self.loop` |
| `faust/tables/sets.py` | 1 | `Manager(self, loop=self.loop)` |
| `faust/worker.py` | 3 | `Worker(loop=)`, `mode.Worker.__init__(loop=)` |
| `faust/cli/base.py` | 5 | `get_event_loop()`, `loop=` params |
| `faust/cli/worker.py` | 2 | `loop` param |
| `faust/web/drivers/aiohttp.py` | 1 | `server_cls(loop=)` |
| `faust/app/_attached.py` | 1 | `loop=self.app.loop` |
| `faust/_cython/streams.pyx` | 2 | `self.loop`, `sleep(0, loop=self.loop)` |

**Affected type interface files (must update signatures):**
- `faust/types/transports.py` (4 occurrences)
- `faust/types/channels.py` (1)
- `faust/types/app.py` (4)
- `faust/types/topics.py` (1)
- `faust/types/streams.py` (1)

**Affected test files:** 49+ occurrences across `t/conftest.py`, `t/unit/`, `t/functional/`.

### 3.2. BREAKING — `NamedTuple._field_types` Removed (Python 3.12)

**File**: `faust/models/typing.py`, line ~408  
**Code**: `tup._field_types.items()`  
**Fix**: Replace with `typing.get_type_hints(tup)` or `tup.__annotations__`

### 3.3. BREAKING — asyncio.Future Subclasses with `loop=`

| Class | File | Issue |
|-------|------|-------|
| `ReplyPromise(asyncio.Future)` | `faust/agents/replies.py` L28 | Accesses `self._loop` (internal attr removed in 3.10) |
| `ProducerProduceFuture(asyncio.Future)` | `faust/transport/drivers/confluent.py` L358 | Instantiated with `loop=self.loop` |
| `FutureMessage(asyncio.Future)` | `faust/types/tuples.py` L93 | No direct `loop=` but depends on internal behavior |

### 3.4. WARNING — `datetime.utcnow()` (Deprecated in 3.12)

- `faust/livecheck/case.py` L232
- `faust/livecheck/models.py` L106
- **Fix**: `datetime.now(timezone.utc)` (both already do `.astimezone(timezone.utc)`)

### 3.5. WARNING — `asyncio.get_event_loop()` Without Running Loop

- `faust/cli/base.py` L616, L635, L653
- `faust/transport/base.py` L63
- `faust/transport/drivers/aiokafka.py` L1768
- **Fix**: Use `asyncio.new_event_loop()` + `asyncio.set_event_loop()` or `asyncio.get_running_loop()`

---

## 4. Phase 0 — Prerequisites & Dependency Audit ✅ COMPLETED

**Goal**: Establish whether dependencies can support Python 3.12 before starting code changes.

**Audit Environment**: Python 3.12.2 venv at `/tmp/faust-py312-audit/`

### Step 0.1: Audit mode-streaming 0.4.x ✅ PASSED
- [x] Install `mode-streaming==0.4.1` in a Python 3.12 venv — **installed successfully**
- [x] Run `import mode; from mode import Service` — **OK**
- [x] Run `from mode.utils.objects import annotations, is_optional` — **OK**
- [x] `Service()` instantiation and start/stop lifecycle — **OK**
- [x] `mode.utils.locks.Event` set/wait/clear — **OK**
- [x] `mode.utils.futures.current_task()` — **OK**
- [x] `Service(loop=None)` backward compat — **OK** (still accepted)
- [ ] ~~**Decision point**: If 0.4.1 still crashes on `loop=`, we must either:~~
  - ~~(a) Fork mode-streaming and remove all `loop=` usage, or~~
  - ~~(b) Vendor the affected `mode.utils` modules into faust and patch them~~

**API Investigation Results (mode-streaming 0.3.5 → 0.4.1):**

| API | 0.4.1 Behavior | Faust Usage | Breaking? |
|-----|----------------|-------------|-----------|
| `FlowControlEvent` | Has `.suspend()`, `.resume()`, `.clear()` | Calls same | **No** |
| `FlowControlQueue.__init__` | `flow_control=` required; `**kwargs` → `asyncio.Queue` | Passes `flow_control=` + `loop=` | **Yes** — `loop=` reaches `asyncio.Queue` |
| `ThrowableQueue` | Inherits `FlowControlQueue` | Same `loop=` propagation | **Yes** — fix by removing `loop=` in Phase 2 |
| `annotations()` | Returns `Tuple[FieldMapping, DefaultsMapping]` | `fields, defaults = annotations(...)` | **No** |
| `Service.__init__` | Accepts `loop=` (stores internally) | Passes `loop=` | **No** |

**Key Finding**: mode-streaming 0.4.1 works on 3.12. The only runtime issue is `loop=` propagating through `**kwargs` from `ThrowableQueue` → `FlowControlQueue` → `asyncio.Queue`, which rejects `loop=` in 3.12. Fix: stop passing `loop=` in faust (Phase 2).

### Step 0.2: Audit core dependencies ✅ ALL RESOLVE

| Package | Latest | Requires | 3.12? | Notes |
|---------|--------|----------|-------|-------|
| mode-streaming | 0.4.1 | >=3.8 | ✅ | Tested |
| aiohttp | 3.13.3 | >=3.9 | ✅ | |
| click | 8.3.1 | >=3.10 | ✅ | Faust pins `<8.0`; 7.1.2 is pure Python |
| psutil | 7.2.2 | >=3.6 | ✅ | **Must unpin from ==5.8.0** |
| aiokafka | 0.13.0 | >=3.10 | ✅ | |
| venusian | 3.1.1 | >=3.7 | ✅ | Faust pins `<2.0` |
| yarl | 1.22.0 | >=3.9 | ✅ | Within `<2.0` |
| croniter | 6.0.0 | >=3.8 | ✅ | |
| intervaltree | 3.1.0 | — | — | Pure Python |
| Others | — | — | ✅ | All resolve |

### Step 0.3: Audit extras dependencies ✅

| Package | Latest | 3.12? | Action |
|---------|--------|-------|--------|
| cchardet | 2.1.7 | **No** | → charset-normalizer 3.4.4 ✅ |
| orjson | 3.11.7 | ✅ | Bump to >=3.0 |
| aredis | 1.1.8 | **No** | → redis 6.2.0 ✅ |
| confluent-kafka | 2.10.0 | ✅ | Bump to >=2.0 |
| python-rocksdb | 0.7.0 | **No** | Risk — consider rocksdict |
| eventlet | — | — | **Drop** (decided) |
| raven | — | — | **Drop** (decided) |

### Step 0.4: Audit test dependencies ✅

| Package | Latest | Action |
|---------|--------|--------|
| pytest | 9.0.2 | Bump from ~=5.2.2 |
| pytest-asyncio | 1.3.0 | Bump from >=0.8 |
| pytest-cov | 6.2.1 | OK |
| mypy | 1.16.0 | OK |
| flake8 | 7.1.2 | OK |

---

## 5. Phase 1 — mode-streaming Upgrade (Critical Blocker)

**Goal**: Get mode-streaming working on Python 3.12.

### Step 1.1: Bump version pin
```diff
- mode-streaming>=0.3.5,<0.4
+ mode-streaming>=0.4.1
```

### Step 1.2: Identify API changes between 0.3.5 → 0.4.x ✅ DONE (Phase 0 audit)
- [x] `annotations()` returns tuple — faust already unpacks correctly
- [x] `Service.__init__` still accepts `loop=`
- [x] `FlowControlEvent` API unchanged (`.suspend()`, `.resume()`, `.clear()`)
- [x] `ThrowableQueue` requires `flow_control=` kwarg — faust already passes it
- [ ] Run faust import tests to identify any `ImportError` or `AttributeError`

### Step 1.3: Evaluate if 0.4.1 actually works on 3.12 at runtime ✅ CONFIRMED
mode-streaming 0.4.1 works on Python 3.12. Service lifecycle, events, locks, typing introspection all pass.

~~### Step 1.4: Fork or monkey-patch mode-streaming~~

*Deferred. Will be tackled as a separate effort only if Step 1.3 confirms 0.4.1 is broken at runtime.*

---

## 6. Phase 2 — asyncio `loop=` Parameter Removal

**Goal**: Remove all `loop=` usage from Faust source code.

This is the largest phase — 105+ occurrences across 25+ files. Must be done **after** Phase 1 (mode-streaming) because many `loop=` params are passed *to* mode classes.

### Step 2.1: Remove `loop` parameter from constructors and type signatures

**Pattern**: Remove `loop: asyncio.AbstractEventLoop = None` parameter, remove `self.loop = loop` assignment, remove `loop=self.loop` passes.

**Order of operations** (types first, then implementations, then consumers):

1. **Type interfaces** (`faust/types/`):
   - [ ] `faust/types/app.py` — remove `loop` from `AppT` abstract methods
   - [ ] `faust/types/channels.py` — remove `loop` from `ChannelT`
   - [ ] `faust/types/topics.py` — remove `loop` from `TopicT`
   - [ ] `faust/types/streams.py` — remove `loop` from `StreamT`
   - [ ] `faust/types/transports.py` — remove `loop` from `TransportT`, `ConsumerT`, `ProducerT`

2. **Core implementations** (bottom-up, starting with leaf dependencies):
   - [ ] `faust/channels.py` — remove `loop` param, remove `self.loop` storage
   - [ ] `faust/topics.py` — remove `loop` param
   - [ ] `faust/streams.py` — remove `loop` param, fix `asyncio.Event()` calls
   - [ ] `faust/transport/base.py` — remove `loop` param and `get_event_loop()` fallback
   - [ ] `faust/transport/consumer.py` — remove `loop` param, fix `asyncio.Future()` calls
   - [ ] `faust/transport/producer.py` — remove `loop` param
   - [ ] `faust/transport/conductor.py` — fix `asyncio.Event()` and `asyncio.Future()` calls
   - [ ] `faust/transport/drivers/aiokafka.py` — **19 occurrences**, most complex file
   - [ ] `faust/transport/drivers/confluent.py` — remove `loop=`, fix `ProducerProduceFuture`

3. **Agent layer**:
   - [ ] `faust/agents/agent.py` — remove `loop=self.loop` from channel/topic creation
   - [ ] `faust/agents/replies.py` — fix `ReplyPromise(asyncio.Future)`, remove `self._loop` access, fix `asyncio.Queue()`

4. **Tables**:
   - [ ] `faust/tables/base.py` — remove `loop=`
   - [ ] `faust/tables/manager.py` — fix `asyncio.Event()` calls
   - [ ] `faust/tables/recovery.py` — fix 3x `Event(loop=self.loop)`
   - [ ] `faust/tables/sets.py` — remove `loop=`

5. **App layer**:
   - [ ] `faust/app/base.py` — **19 occurrences**: remove `loop` from `__init__`, fix `FlowControlEvent()`, `ThrowableQueue()`, service init calls
   - [ ] `faust/app/_attached.py` — remove `loop=self.app.loop`

6. **Worker & CLI**:
   - [ ] `faust/worker.py` — remove `loop` param, fix `mode.Worker.__init__()` call
   - [ ] `faust/cli/base.py` — refactor event loop management (replace `get_event_loop()` with `new_event_loop()`)
   - [ ] `faust/cli/worker.py` — remove `loop` param from `as_service()`

7. **Web**:
   - [ ] `faust/web/drivers/aiohttp.py` — remove `loop=` from server construction

### Step 2.2: Fix asyncio.Future subclasses

- [ ] `faust/agents/replies.py` — `ReplyPromise(asyncio.Future)`:
  - Remove `self._loop` access in `BarrierState.__post_init__`
  - Ensure constructor doesn't pass `loop=`
- [ ] `faust/transport/drivers/confluent.py` — `ProducerProduceFuture(asyncio.Future)`:
  - Remove `loop=` from instantiation

### Step 2.3: Replace `asyncio.get_event_loop()` where no loop is running

- [ ] `faust/cli/base.py` L616 — use `asyncio.new_event_loop()` + `set_event_loop()`
- [ ] `faust/transport/base.py` L63 — use `asyncio.get_running_loop()` (if called from async context) or remove fallback

---

## 7. Phase 3 — Typing & Model System Fixes

**Goal**: Fix typing introspection that breaks on Python 3.12.

### Step 3.1: Fix `NamedTuple._field_types` (P0)
**File**: `faust/models/typing.py`, line ~408

```python
# BEFORE (broken in 3.12):
for field_name, field_type in tup._field_types.items():

# AFTER:
for field_name, field_type in typing.get_type_hints(tup).items():
```

### Step 3.2: Verify mode-streaming typing introspection
After Phase 1, verify these still work on 3.12:
- [ ] `mode.utils.objects.annotations()` — calls `typing.get_type_hints()` internally
- [ ] `mode.utils.objects.is_optional()` — checks `__origin__` and `__args__`
- [ ] `mode.utils.objects.is_union()` — must handle both `typing.Union` and `types.UnionType` (PEP 604, `X | Y`)
- [ ] `mode.utils.objects.remove_optional()` — same as above

### Step 3.3: Handle `types.UnionType` (Python 3.10+)
Python 3.10+ introduced `X | Y` syntax producing `types.UnionType` instead of `typing.Union`. If any code uses the `|` syntax for type hints, `is_union()` and `is_optional()` may fail.

- [ ] Check if Faust uses `X | Y` type annotations anywhere (unlikely given 3.7 baseline)
- [ ] If bumping minimum Python version, ensure mode-streaming handles `types.UnionType`

### Step 3.4: Verify `__class_getitem__` patterns
- [ ] `faust/models/tags.py` — `Personal.__class_getitem__` and `Sensitive.__class_getitem__` — verify on 3.12

---

## 8. Phase 4 — Cython Extension Rebuild

**Goal**: Ensure C extensions compile and run on Python 3.12.

### Step 4.1: Fix `faust/_cython/streams.pyx`
```python
# BEFORE (broken):
await sleep(0, loop=self.loop)

# AFTER:
await sleep(0)
```
Also remove `self.loop = self.stream.loop` storage.

### Step 4.2: Update Cython version
- [ ] Ensure `Cython>=3.0.0` is used (required for Python 3.12 C API changes)
- [ ] Python 3.12 changed `PyTypeObject` struct layout, removed `tp_print`, changed `Py_TPFLAGS`

### Step 4.3: Recompile all extensions
- [ ] `faust/_cython/streams.pyx`
- [ ] `faust/_cython/windows.pyx`
- [ ] `faust/transport/_cython/conductor.pyx`
- [ ] Verify with `python setup.py build_ext --inplace`

### Step 4.4: Test with Cython disabled
- [ ] Set `NO_CYTHON=1` and verify pure-Python fallback works on 3.12

---

## 9. Phase 5 — Deprecation Warnings & Cleanup

**Goal**: Fix remaining deprecation warnings so the codebase is clean on 3.12.

### Step 5.1: `datetime.utcnow()` → `datetime.now(timezone.utc)`
- [ ] `faust/livecheck/case.py` L232
- [ ] `faust/livecheck/models.py` L106

### Step 5.2: Update `setup.py`
- [ ] Update `python_requires='>=3.7.0'` to `python_requires='>=3.10'`
- [ ] Update classifiers to list 3.10, 3.11, 3.12, 3.13 (remove 3.7, 3.8, 3.9)
- [ ] Verify `setuptools.errors` imports still work with latest setuptools

### Step 5.3: Update `setup.cfg`
- [ ] `mypy` → `python_version = 3.6` — bump to `3.12`
- [ ] Remove or update any 3.6-specific config

---

## 10. Phase 6 — Test Infrastructure & Validation

**Goal**: Get the test suite running on Python 3.12.

### Step 6.1: Bump test dependencies
```diff
- pytest~=5.2.2
+ pytest>=7.4.0
- pytest-asyncio>=0.8
+ pytest-asyncio>=0.23.0
```

### Step 6.2: Fix test `loop=` usage (49+ occurrences)
- [ ] `t/conftest.py` — `all_tasks(loop=loop)` and fixture `loop=` params
- [ ] `t/functional/agents/helpers.py` — `asyncio.Event(loop=)` in test helpers
- [ ] `t/functional/test_streams.py` — `loop=` in test setup
- [ ] `t/unit/test_worker.py` — `loop=` in Worker tests
- [ ] `t/unit/cli/test_base.py` — `loop=` in CLI tests
- [ ] `t/unit/agents/test_agent.py` — `loop=` in agent tests
- [ ] `t/unit/app/test_base.py` — `loop=` in app tests
- [ ] `t/unit/transport/drivers/test_aiokafka.py` — `loop=` in transport tests
- [ ] All other test files with `loop=`

### Step 6.3: Validation plan
1. **Import smoke test**: `python -c "import faust; print(faust.__version__)"`
2. **Unit tests**: `pytest t/unit/ -x --timeout=60`
3. **Functional tests**: `pytest t/functional/ -x --timeout=120`
4. **Integration test**: Start a Faust worker connected to Kafka, send messages, verify processing
5. **Kaspr integration**: Run kaspr with the updated faust on Python 3.12

---

## 11. Phase 7 — Dependency Version Bumps

**Goal**: Update all dependency constraints for Python 3.12 compatibility.

### `requirements/default.txt` changes:
```diff
  aiohttp>=3.8.0,<4.0
  aiohttp_cors>=0.7,<2.0
- click>=6.7,<8.0
+ click>=7.0,<9.0
  colorclass>=2.2,<3.0
- mode-streaming>=0.3.5,<0.4
+ mode-streaming>=0.4.1
  opentracing>=1.3.0,<2.0.0
  terminaltables>=3.1,<4.0
- venusian>=1.1,<2.0
+ venusian>=1.1
- yarl>=1.0,<2.0
+ yarl>=1.0,<3.0
  croniter>=0.3.16
  mypy_extensions
  intervaltree==3.1.0
- psutil==5.8.0
+ psutil>=5.9.5
  aiokafka>=0.12.0,<1.0
  sortedcontainers>=2.0.0
```

### `requirements/extras/` changes:
```diff
# cchardet.txt
- cchardet>=2.1
+ charset-normalizer>=3.0

# orjson.txt
- orjson>=2.0,<3.0  
+ orjson>=3.0

# redis.txt
- aredis>=1.1.3,<2.0
+ redis>=4.0

# ckafka.txt  
- confluent-kafka~=1.2.0
+ confluent-kafka>=2.0

# eventlet.txt
# DROP ENTIRELY (eventlet is abandoned, no 3.12 support)

# sentry.txt
# DROP ENTIRELY (raven is deprecated)
```

Also remove `eventlet` and `sentry` from the `BUNDLES` set in `setup.py`.

---

## 12. Risk Assessment & Decision Points

### Decision 1: mode-streaming Strategy ✅ DECIDED

**Decision**: Bump to `mode-streaming>=0.4.1`. If 0.4.1 does not work at runtime, we will fork the project — but that fork effort will be tackled separately.

| Option | Effort | Risk | Status |
|--------|--------|------|--------|
| Bump to 0.4.1 and test | Low | Medium — may still have runtime `loop=` crashes | **✅ Primary approach** |
| Fork mode-streaming | Medium | Low — full control | **Fallback (separate effort)** |
| ~~Vendor affected modules~~ | ~~Medium~~ | ~~Medium~~ | Rejected |
| ~~Replace mode-streaming entirely~~ | ~~Very High~~ | ~~High~~ | Rejected |

### Decision 2: Minimum Python Version ✅ DECIDED

**Decision**: `python_requires='>=3.10'`. Target production runtime is Python 3.12+ (new base image post-migration).

| Option | Impact | Status |
|--------|--------|--------|
| ~~Keep `>=3.7`~~ | ~~Must maintain backward compat for old asyncio patterns~~ | Rejected |
| ~~Bump to `>=3.9`~~ | ~~Can use `asyncio.get_running_loop()`~~ | Rejected |
| Bump to `>=3.10` | Clean `loop=` removal (no shims), `X \| Y` union types available | **✅ Chosen** |
| ~~Bump to `>=3.12`~~ | ~~Cleanest, but drops 3.10/3.11~~ | Rejected |

Since `loop=` was removed in Python 3.10, every removal is a clean delete — no conditional logic or version checks needed.

### Decision 3: Cython Extensions ✅ DECIDED

**Decision**: Keep Cython extensions. The fix is minimal (remove `loop=` in `streams.pyx`, recompile with Cython >=3.0). The pure-Python fallback in `setup.py` already exists. Extensions accelerate hot paths (stream iteration, windows, conductor).

| Option | Impact | Status |
|--------|--------|--------|
| Keep Cython, update for 3.12 | Must use Cython >=3.0, need CI for building wheels | **✅ Chosen** |
| ~~Drop Cython entirely~~ | ~~Slight performance loss, simpler maintenance~~ | Rejected |

### Decision 4: Deprecated Extras ✅ DECIDED

**Decision**: Drop `eventlet` and `raven/sentry` extras.

| Extra | Status | Decision |
|-------|--------|----------|
| `eventlet` | Broken, abandoned | **✅ Drop** |
| `rocksdb` | Likely broken on 3.12 | Keep if `rocksdict` works, otherwise drop |
| `sentry` (raven) | Deprecated | **✅ Drop** |

---

## 13. Verification Checklist

### Pre-flight
- [ ] Python 3.12 venv created
- [ ] All dependencies install without errors
- [ ] `import faust` succeeds
- [ ] `import mode` succeeds

### Unit Tests
- [ ] `pytest t/unit/ -x` passes
- [ ] No `DeprecationWarning` from asyncio
- [ ] No `DeprecationWarning` from datetime

### Functional Tests
- [ ] `pytest t/functional/ -x` passes
- [ ] Stream processing works end-to-end
- [ ] Agent lifecycle (start → process → stop) works

### Integration (Kaspr)
- [ ] Kaspr operator connects to Kafka via faust on Python 3.12
- [ ] Messages are produced and consumed correctly
- [ ] Table state is maintained across restarts
- [ ] Web views respond correctly

### Performance
- [ ] Benchmark Cython vs pure-Python on 3.12
- [ ] No regression in message throughput

---

## Appendix A: File Change Summary by Phase

| Phase | Files Modified | Estimated Changes |
|-------|---------------|-------------------|
| Phase 0 | 0 (audit only) | — |
| Phase 1 | 1-3 (requirements, possibly mode patches) | Depends on strategy |
| Phase 2 | 25+ source files, 10+ test files | 105+ `loop=` removals |
| Phase 3 | 1-2 files | `_field_types` fix, typing verification |
| Phase 4 | 3 .pyx files, `setup.py` | Cython fixes + rebuild |
| Phase 5 | 4 files | Minor cleanups |
| Phase 6 | 10+ test files | Test infra + `loop=` removal in tests |
| Phase 7 | 2-3 requirements files | Version bumps |

**Total estimated files**: ~50+ files to modify
**Total estimated line changes**: ~300-500 lines

## Appendix B: Execution Order

```
Phase 0 (Audit)
    │
    ▼
Phase 1 (mode-streaming) ◄── CRITICAL GATE: must succeed before continuing
    │
    ├──► Phase 2 (loop= removal)      ──► Phase 6 (Tests)
    │                                          │
    ├──► Phase 3 (Typing fixes)                │
    │                                          │
    ├──► Phase 4 (Cython)                      │
    │                                          │
    ├──► Phase 5 (Deprecations)                │
    │                                          │
    └──► Phase 7 (Dependency bumps)            │
                                               ▼
                                        Integration Testing
```

Phase 1 is the gate. Phases 2-5 and 7 can be done in parallel after Phase 1 is resolved. Phase 6 must come after Phase 2 (since tests use the same `loop=` patterns as source).
