Skip to content

Type-check the whole faust package and delete the mypy ratchet - #758

Merged
wbarnha merged 5 commits into
masterfrom
claude/faust-mypy-compat-xyb41h
Aug 6, 2026
Merged

Type-check the whole faust package and delete the mypy ratchet#758
wbarnha merged 5 commits into
masterfrom
claude/faust-mypy-compat-xyb41h

Conversation

@wbarnha

@wbarnha wbarnha commented Aug 6, 2026

Copy link
Copy Markdown
Member

mypy -p faust now checks all 164 modules in the package with nothing silenced wholesale, so the ignore_errors ratchet in pyproject.toml is gone — all 78 entries, not a subset.

Measured with the pinned mypy 2.3.0 from requirements/typecheck.txt, in an environment matching the lint job (the installed dependency set changes what mypy reports, since absent packages become Any):

errors with the ratchet disabled
master (4af976b) 512
after the implicit-Optional codemod 303
after this branch 0

The commits

  1. Make implicit Optional parameters explicit — PEP 484 dropped implicit Optional and mypy has defaulted to no_implicit_optional=True since 0.990, so every x: T = None parameter was an error. That single pattern was 269 of the 512, across 62 files. Rewritten with hauntsaninja's no_implicit_optional codemod, then scripts/lint to normalise the imports it added.
  2. Correct the type-level contracts in faust/types — the protocol definitions the rest of the package implements, fixed first so implementations are not annotated against signatures that are themselves wrong.
  3. Type-check the rest of faust and delete the ratchet — the remaining implementation modules, plus the ratchet removal.
  4. Keep the aerospike type-ignores on the interpolating line — PEP 701 changed f-string tokenisation in 3.12, so mypy attributes an error inside an implicitly-concatenated f-string to a different fragment depending on the interpreter. python_version = "3.12" pins the typeshed target but not the parser, so three ignores that looked correct on 3.11 missed their line on the lint job. Joined into single physical lines so the interpolation and the ignore cannot separate.
  5. Require mode-streaming>=0.6.0 — see below.

Annotation-only

No runtime behaviour differs from master. Where mypy was pointing at a genuine defect, the code is left exactly as it was and the error is silenced with a specific # type: ignore[code] whose comment says — prefixed XXX — what is actually broken, so the checker stops flagging it without the diff quietly pretending it is fine.

Those XXX markers are the useful residue of this pass. Each is a real bug that deserves its own fix and test, and none is addressed here:

  • aiokafka.pyProducerThread._shutdown_thread is a sync override of mode's async def, which _serve() awaits in a finally:. Shutdown therefore evaluates await None and raises TypeError.
  • aiokafka.pyTopic._on_published is called without its required positional fut, so the wait-branch raises TypeError.
  • confluent.pyProducer.key_partition reaches for list_topics on the Faust producer rather than the confluent one, so it is dead on arrival.
  • tables/base.py_partition_timestamp_keys is looked up with the whole (start, end) window range where the map is keyed on end, so on_window_close never receives the aggregated window data.
  • recovery.py — a changelog event for an untracked TP is applied using the previous loop iteration's table/offsets/bufsize.
  • auth.pySSLCredentials() defaults purpose to None, which ssl.create_default_context rejects with TypeError.
  • cli/base.pyAppCommand.__init__ dereferences self.app.conf unconditionally, so the documented require_app = False escape hatch raises AttributeError.
  • faust/__init__.pyversion_info is built from regex string groups, so .major is the literal 'v'.

Why the mode floor moved

mode ships PEP 561 type information, so its annotations are part of faust's own type-check surface rather than an opaque Any. That is why mypy can see into Service/ServiceThread at all — and why the result depends on which mode is installed. On this tree:

mode-streaming mypy -p faust
0.4.1 38 errors in 14 files
0.6.0 0 errors

The difference is annotation precision in mode, not anything faust does: 0.4.1 declares want_seconds(float) where the argument is really Seconds, and level_name(int) where it is str | int. requirements.txt said >=0.4.0, so a contributor resolving an older mode saw 38 errors on a tree the lint job calls clean — the same reproducibility problem the repo already avoids by pinning mypy. The floor now matches what CI has been resolving all along.

The one carve-out

faust.types.settings.settings keeps a narrow, documented disable_error_code = ["empty-body"]. Every setting there is a docstring-only method consumed by the @sections.<Section>.setting(...) decorator, which discards the function and returns a Param descriptor — the body is never executed, so the check does not apply to the pattern. That disables one error code, not the module: every other error in the file is still reported, which is a strict improvement on the ignore_errors entry it replaces.

mypy stays dev/CI-only

Unchanged by this PR, but worth stating since it is the point of the ratchet: setup.py sets install_requires=reqs("requirements.txt"), and mypy lives in requirements/typecheck.txt, reachable only via test.txttests_require, which pip does not install for end users. extras_require() only globs requirements/extras/, so it cannot leak in there either. Nothing under faust/ imports mypy.

Verification

scripts/check (isort, black, flake8, mypy) passes with the pinned toolchain from requirements/test.txt, and the unit and functional suites are unchanged at 2207 passed, 4 skipped.

On coverage: codecov/project passes at +0.01% — Hits +10, Misses −2 against master. codecov/patch reports 89.03%; its missing lines are lines this diff touched in code CI cannot execute — the aerospike and rocksdb drivers (optional deps not installed, unlike confluent.py which .coveragerc already omits for that reason) and cast()/type-ignore lines inside defensive branches that were already uncovered on master. Writing tests for defensive error paths was out of scope for an annotation-only change.

🤖 Generated with Claude Code

claude added 2 commits August 5, 2026 23:00
PEP 484 dropped implicit Optional and mypy has defaulted to
`no_implicit_optional=True` since 0.990, so every `x: T = None` parameter in
the package was an error.  That single pattern accounted for the largest
share of the backlog the ratchet in pyproject.toml is silencing -- 269 of the
512 errors mypy reports with the ratchet disabled, spread over 62 files.

Rewrite them as `x: Optional[T] = None` with hauntsaninja's
`no_implicit_optional` codemod, then re-run `scripts/lint` so isort and black
normalise the imports it added.  The transformation is annotation-only: it
widens each parameter's declared type to include the `None` that the default
already allowed, so no call that type-checked before stops doing so, and no
runtime behaviour changes.  `scripts/check` and the unit and functional
suites all still pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012N2yysiNbzzVcvYhtrpsM7
These are the protocol definitions the rest of the package implements, so
fixing them first keeps the implementation modules from being annotated
against signatures that are themselves wrong.

`Param.__get__` returned `OT` unconditionally and needed a `# type: ignore` for
the `obj is None` branch that hands back the descriptor.  Declare the two cases
as overloads instead, mirroring the split `property` already declares, and drop
the ignore.

`Param.prepare_set`, `prepare_init_default`, `on_set` and `validate_after` all
claimed to take and return a non-optional value, but every one of them handles
`None` explicitly -- `prepare_set` returns it unchanged when `allow_none` is
set.  Widen them to `Optional` so the declared contract matches what the code
actually does.

`StreamT.__next__` returned a bare `T`, a type variable that appears nowhere
else in the class; `StreamT` is generic in `T_co`.  It was meaningless as
written, so bind it to `T_co`, which is covariant and so valid in return
position.

`AppT.conf`'s getter was a `...`-bodied property that mypy read as a concrete
method returning nothing.  Mark it abstract, which is what it always was --
`App` provides the concrete property.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012N2yysiNbzzVcvYhtrpsM7
@codecov

codecov Bot commented Aug 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.03226% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.07%. Comparing base (4af976b) to head (e1e250d).

Files with missing lines Patch % Lines
faust/transport/producer.py 45.45% 4 Missing and 2 partials ⚠️
faust/stores/aerospike.py 87.50% 2 Missing ⚠️
faust/stores/rocksdb.py 75.00% 2 Missing ⚠️
faust/streams.py 88.88% 2 Missing ⚠️
faust/tables/manager.py 0.00% 2 Missing ⚠️
faust/app/base.py 87.50% 1 Missing ⚠️
faust/transport/consumer.py 87.50% 1 Missing ⚠️
faust/web/cache/backends/redis.py 66.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #758      +/-   ##
==========================================
+ Coverage   96.06%   96.07%   +0.01%     
==========================================
  Files         103      103              
  Lines       11072    11081       +9     
  Branches     1191     1189       -2     
==========================================
+ Hits        10636    10646      +10     
+ Misses        345      343       -2     
- Partials       91       92       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

`mypy -p faust` now checks all 164 modules in the package with nothing silenced
wholesale, so the `ignore_errors` ratchet in pyproject.toml is gone -- all 78
entries, not a subset.

The previous commit cleared the implicit-Optional backlog; this one fixes what
was left.  The bulk is honest annotation work: attributes declared on the class
that were only ever assigned in `__init__`, `Optional` added where a value
really can be None, protocol signatures corrected to match every implementer,
and generic bases parameterised (`Schema(SchemaT[KT, VT])`,
`TopicBuffer(Iterator[Tuple[TP, Any]])`) so their element types survive to
callers.

The change is annotation-only by construction: no runtime behaviour differs
from the previous commit.  Where the checker was pointing at a genuine defect,
the code is left exactly as it was and the error is silenced with a specific
`# type: ignore[code]` whose comment says, prefixed `XXX`, what is actually
broken -- so the checker stops flagging it without the diff quietly pretending
it is fine.  Those XXX markers are the useful residue of this pass; each one is
a real bug worth its own fix and test:

  - aiokafka.py `ProducerThread._shutdown_thread` is a sync override of mode's
    `async def`, which `_serve()` awaits in a `finally:` -- so shutdown hits
    `await None` and raises TypeError.
  - aiokafka.py calls `Topic._on_published` without its required positional
    `fut`, so the wait-branch raises TypeError.
  - confluent.py `Producer.key_partition` reaches for `list_topics` on the
    Faust producer rather than the confluent one, so it is dead on arrival.
  - tables/base.py keys `_partition_timestamp_keys` lookups with the whole
    `(start, end)` window range where the map is keyed on `end`, so
    `on_window_close` never receives the aggregated window data.
  - recovery.py applies a changelog event for an untracked TP using the
    previous iteration's `table`/`offsets`/`bufsize` locals.
  - auth.py `SSLCredentials()` defaults `purpose` to None, which
    `ssl.create_default_context` rejects with TypeError.
  - cli/base.py `AppCommand.__init__` dereferences `self.app.conf`
    unconditionally, so the documented `require_app = False` escape hatch
    raises AttributeError.
  - `faust.version_info` is built from regex string groups, so `.major` is the
    literal 'v'.

`faust.types.settings.settings` keeps a narrow, documented
`disable_error_code = ["empty-body"]`: every setting there is a docstring-only
method consumed by the `@sections.<Section>.setting(...)` decorator, which
discards the function and returns a descriptor, so the body is never executed.
That is one error code, not the module -- every other error in the file is
still reported.

`scripts/check` (isort, black, flake8, mypy) passes with the pinned toolchain
from requirements/test.txt, and the unit and functional suites are unchanged at
2207 passed, 4 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012N2yysiNbzzVcvYhtrpsM7
@wbarnha wbarnha changed the title Make implicit Optional parameters explicit Type-check the whole faust package and delete the mypy ratchet Aug 6, 2026
claude added 2 commits August 6, 2026 11:47
The lint job failed on the previous commit with three `str-bytes-safe`
errors in faust/stores/aerospike.py that do not reproduce under Python
3.11.

Each was an implicitly-concatenated f-string whose `# type: ignore` sat on
the first fragment while the `{key}` interpolation that actually trips the
check sat on the second:

    self.log.error(
        f"FaustAerospikeException Error in set for "  # type: ignore[str-bytes-safe]
        f"table {self.table_name} exception {ex} key {key}"
    )

PEP 701 changed f-string tokenisation in 3.12, so mypy attributes the error
to a different fragment depending on the interpreter it runs under -- the
first fragment on 3.11, the second on 3.12.  `python_version = "3.12"` in
pyproject.toml pins the typeshed target but not the parser, so a contributor
on 3.11 sees a clean tree while the lint job (PYTHON_LATEST = 3.12) does not.

Join each of the three into a single physical line so the interpolation and
the ignore cannot land on different lines under any interpreter.  The
concatenated message text is unchanged, so the log output is identical.  A
scan of the package confirms no other `# type: ignore` sits on an implicitly
concatenated string.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012N2yysiNbzzVcvYhtrpsM7
mode ships PEP 561 type information, so its annotations are part of faust's
own type-check surface rather than an opaque `Any`.  That cuts both ways: it
is why mypy can see through into `Service`/`ServiceThread` at all, and it is
why the result depends on which mode a contributor happens to have installed.

Measured on this tree with the pinned mypy 2.3.0:

    mode-streaming 0.4.1   ->  38 errors in 14 files
    mode-streaming 0.6.0   ->  0 errors

The difference is annotation precision in mode, not anything faust does.
0.4.1 declares `want_seconds(float)` where the argument is really `Seconds`
(`timedelta | float | str`) and `level_name(int)` where it is `str | int`, and
its generic containers infer less, so `channel_it` and friends need explicit
annotations that 0.6.0 makes unnecessary.

`requirements.txt` said `>=0.4.0`, so a contributor resolving an older mode saw
38 errors on a tree the lint job calls clean -- exactly the reproducibility
problem the repo already avoids by pinning mypy in typecheck.txt.  Raise the
floor to the version CI has been resolving and testing against all along.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012N2yysiNbzzVcvYhtrpsM7
@wbarnha
wbarnha merged commit 63c3651 into master Aug 6, 2026
29 of 30 checks passed
wbarnha pushed a commit that referenced this pull request Aug 6, 2026
Both were found by the type checker in #758 and left marked `XXX` there
because fixing them changes runtime behaviour.

`ThreadedProducer._shutdown_thread` was a plain `def` overriding
`mode.threads.ServiceThread._shutdown_thread`, which is `async def` and is
awaited by `_serve()` in a `finally:`.  Every shutdown of the producer thread
therefore evaluated `await None` and raised TypeError.  The thread only
recovered because `_start_thread` catches that exception and calls
`set_shutdown()` before re-raising -- so mode's teardown
(`on_thread_stop`, stopping children, futures and exit stacks) never ran, and
the thread died with a traceback instead of stopping cleanly.

The override also scheduled `on_thread_stop()` with
`asyncio.run_coroutine_threadsafe` onto `self.thread_loop` -- the loop that
was about to stop, and the loop already running `_serve()`.  Because the
TypeError tore down `run_until_complete` immediately, that coroutine never got
a chance to run, so the producer was never flushed or stopped on this path.

Make it `async def` and await `super()._shutdown_thread()`, which runs
`on_thread_stop()` and the rest of mode's teardown in order.  The once-only
guard is kept; when shutdown has already been initiated the shutdown event is
still set, matching what the old TypeError path ended up doing via
`_start_thread`.

`ThreadedProducer.publish_message(wait=True)` called
`fut.message.channel._on_published(message=..., state=..., producer=...)`.
`Topic._on_published` takes the send future as a required *positional* `fut`
and reads the result off it, so the call raised
`TypeError: Topic._on_published() missing 1 required positional argument`.
The waiting branch has no such future -- `send_and_wait` has already resolved
-- so complete the message directly instead: report the sensor, set the
result, and invoke the callback, which is what `Topic.publish_message(wait=True)`
does via `_finalize_message`.  The non-waiting branch keeps using
`_on_published` as a done-callback, where `add_done_callback` supplies `fut`.

`test_publish_message_with_wait` did not catch this because its channel is a
bare `Mock`, which accepts any call; the new test uses a real topic and fails
with the TypeError above against the previous code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012N2yysiNbzzVcvYhtrpsM7
wbarnha pushed a commit that referenced this pull request Aug 6, 2026
`SSLCredentials.__init__` defaulted `purpose` to None and passed it straight
into `ssl.create_default_context(purpose=...)`, which begins

    if not isinstance(purpose, _ASN1Object):
        raise TypeError(purpose)

So `SSLCredentials()` and `SSLCredentials(cafile=...)` -- any call that does not
supply an explicit `context` -- raised `TypeError: None`.  The class could only
ever be constructed by handing it a context built elsewhere, which defeats the
cafile/capath/cadata parameters entirely.

Default to `ssl.Purpose.SERVER_AUTH`: the same default `create_default_context()`
itself applies, and the correct one for a client verifying a broker.  It implies
`check_hostname=True` and `verify_mode=CERT_REQUIRED`, which the tests assert.
An explicitly passed `purpose` is still forwarded unchanged.

Found by the type checker in #758 and marked `XXX` there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012N2yysiNbzzVcvYhtrpsM7
wbarnha pushed a commit that referenced this pull request Aug 6, 2026
`VersionInfo` declares `major`, `minor` and `micro` as ints, but the module
splatted the regex groups `(prefix, version, suffix)` into them positionally:

    VersionInfo(major=None, minor='0.11.5', micro='')

So `faust.version_info.major` was the `'v'` prefix or None, `.minor` was the
whole version string, `.micro` was the suffix, and `.releaselevel` was always
None -- every field wrong except by accident.

Parse the dotted numbers properly instead, putting any non-numeric tail
(`dev1+g1234`, `rc1`, a local segment) into `releaselevel`.  Missing components
pad with zero, and an unparsable version degrades to `VersionInfo(0, 0, 0, ...)`
rather than raising, so `import faust` can never fail on the version string --
the old `RuntimeError('THIS IS A BROKEN RELEASE!')` branch is gone with it.

This changes a public value, which is why #758 left it marked `XXX` rather than
fixing it: code reading `faust.version_info.minor` as the version *string* must
switch to `faust.__version__`, which is unchanged.  Nothing in the repo consumes
it and no docs reference it.

`_parse_version` is injected into the lazy module's `__dict__` so it is
reachable for testing; it is private and stays out of `__all__` and `dir()`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012N2yysiNbzzVcvYhtrpsM7
wbarnha pushed a commit that referenced this pull request Aug 6, 2026
`Collection._del_old_keys` read `_partition_timestamp_keys` with

    self._partition_timestamp_keys.get((partition, window_range))

where `window_range` is the whole `(start, end)` tuple.  The map is keyed
`(partition, range_end)` -- an `(int, float)` pair -- written that way by
`_maybe_set_key_ttl` and read that way by `_maybe_del_key_ttl`.

So the lookup could never hit.  `triggered_windows` was always `[None, ...]`,
`window_data` stayed empty, and `on_window_close` was only ever handed the raw
per-key value instead of the aggregated window data it exists to receive.

Use `(partition, window_range[1])`, matching the writer.

This is user-visible: applications with an `on_window_close` handler will start
receiving the aggregated data the API always promised.  That is why #758 marked
it `XXX` instead of fixing it.

Two existing tests relied on `mock_ranges` returning bare floats, which is not
what `_window_ranges` yields; they now pass real `(start, end)` tuples.  Their
assertions are unchanged and the ranges still match nothing in
`_partition_timestamp_keys`, so they continue to cover the untriggered path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012N2yysiNbzzVcvYhtrpsM7
wbarnha pushed a commit that referenced this pull request Aug 6, 2026
Two bugs in `Recovery`, both found by the type checker in #758.

`_slurp_changelogs` classifies each event's TP as active or standby and binds
`table`, `offsets` and `bufsize` accordingly.  The `else:` branch for a TP that
is neither only logged `"recovery unknown topic"` and fell through -- so the
event was applied using the *previous* iteration's bindings: written into an
unrelated table's buffer and offset map, and passed to that table's
`on_changelog_event`.  On the first event of the loop there is nothing bound
yet, so it raised `UnboundLocalError` instead.

Skip applying an event for an untracked TP.  Note a bare `continue` would be
wrong: the statements at the bottom of the loop body -- `_maybe_signal_recovery_end()`
and the standby-ready bookkeeping -- must keep running on every iteration, or
recovery-end signalling loses a trigger.  Only the event-application block is
skipped.

`detect_aborted_tx` compared `await self.app.consumer.position(tp) >= highwater`
unguarded.  `ConsumerT.position` is `Optional[int]` and does return None when a
partition has no position yet, so that raised TypeError -- swallowed by the
caller's `except Exception`, which then silently skipped the aborted-transaction
fixup for every remaining partition in the loop.  Skip a TP with no position.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012N2yysiNbzzVcvYhtrpsM7
wbarnha pushed a commit that referenced this pull request Aug 6, 2026
`_app_from_str` returns None for a `require_app = False` command invoked
without `-A` -- that is the documented escape hatch, and `faust completion` is
exactly such a command.  `_finalize_app` handed that None straight back, and
`AppCommand.__init__` then did

    self.key_serializer = key_serializer or self.app.conf.key_serializer

unconditionally, so the command died with
`AttributeError: 'NoneType' object has no attribute 'conf'`.  The escape hatch
was unusable: `faust completion` could not run without the `-A` it is written
not to need.

Make `AppCommand` tolerate having no app.  `self.app` becomes a property over
an `Optional[AppT]`, the serializer defaults fall back to None when there is no
app, and `on_stop` and `blocking_timeout` no longer assume one.  Behaviour with
an app present is unchanged, and a command with `require_app = True` still gets
the same `UsageError` from `_app_from_str` as before.

Found by the type checker in #758 and marked `XXX` there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012N2yysiNbzzVcvYhtrpsM7
wbarnha pushed a commit that referenced this pull request Aug 6, 2026
`Producer.key_partition` called `self._producer_thread.producer.list_topics()`.
`ProducerThread.producer` is the *Faust* Producer; the confluent_kafka handle is
`ProducerThread._producer`.  Faust producers have no `list_topics`, so the
method raised AttributeError and was dead on arrival.  Read the confluent
producer instead.

`Consumer.verify_event_path` delegated to `self._thread.verify_event_path(...)`,
but neither `ConsumerThread` nor `ConfluentConsumerThread` defines it, so the
commit-livelock detector (`_commit_livelock_detector` ->
`verify_all_partitions_active`) raised AttributeError on every tick.  Add the
no-op to `ConfluentConsumerThread`, matching the documented no-op stub the base
`faust.transport.consumer.Consumer.verify_event_path` already is.  This makes
livelock detection inert for this driver rather than raising -- a real
implementation is separate work.

Both were found by the type checker in #758 and marked `XXX` there.

Note these tests do not run here or in CI: tests/unit/transport/drivers/test_confluent.py
starts with `pytest.importorskip("confluent_kafka")`, and confluent-kafka is the
optional `faust[ckafka]` extra, which the CI test environment does not install.
The fixes are verified by reading the class definitions, not by an executed test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012N2yysiNbzzVcvYhtrpsM7
wbarnha pushed a commit that referenced this pull request Aug 6, 2026
`mypy -p faust` now checks the whole package (#758 removed the ratchet), and
this branch fails it:

    faust/events.py:174: error: Incompatible default for parameter "timestamp"
    (default has type "object", parameter has type "float | None")  [assignment]

The three older sentinels get away with a bare `object()` because the
parameters they default are annotated loosely -- `key: K` and `value: V`
resolve through `Any`, and `headers: Any` is `Any` outright.  `timestamp` is
`Optional[float]`, which `object()` does not inhabit.

Cast the sentinel to `float` rather than widening the parameter: the
annotation callers see stays `Optional[float]`, which is the useful one, and
`typing.cast` is a no-op at runtime, so `USE_EXISTING_TIMESTAMP` is still a
unique object and the `is` comparison in `forward()` is unchanged.

Verified: merged into master, `mypy -p faust` reports "Success: no issues
found in 164 source files"; tests/unit/test_events.py and
tests/functional/test_streams.py pass (61 passed).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012N2yysiNbzzVcvYhtrpsM7
wbarnha pushed a commit that referenced this pull request Aug 6, 2026
`mypy -p faust` now checks the whole package (#758 removed the ratchet), and
this branch fails it with 8 errors -- two implicit-Optional parameters, each
producing one `[assignment]` plus one `[override]` against every supertype
that declares it:

    faust/sensors/otel.py:320: error: Argument 5 of "on_stream_event_out" is
    incompatible with supertype "faust.sensors.monitor.Monitor"; supertype
    defines the argument type as "dict[Any, Any] | None"  [override]
    ... and the same against Sensor and SensorInterfaceT
    faust/sensors/otel.py:320: error: Incompatible default for parameter
    "state" (default has type "None", parameter has type "dict[Any, Any]")
    [assignment]
    ... and the same four for "view" at :441

PEP 484's implicit-Optional was removed in mypy 0.990, so `state: Dict = None`
no longer means `Optional[Dict]`; it means `Dict`, which both rejects the
`None` default and narrows the parameter relative to the base class.  The
bodies already handle `None` (`if state is not None:`) and the base
signatures already say `Optional`, so spelling it out is the correction, not
a behaviour change.

Verified: merged into master, `mypy -p faust` reports "Success: no issues
found in 165 source files"; tests/unit/sensors/test_otel.py passes (18
passed, with opentelemetry installed -- it importorskips otherwise).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012N2yysiNbzzVcvYhtrpsM7
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