Repository navigation
perf!: slot the public objects user code constructs, receives or holds - #3230
Merged
Merged
Conversation
Lancetnik
requested review from
IvanKirpichnikov,
Sehat1137,
borisalekseev and
powersemmi
as code owners
September 23, 2026 19:22
Lancetnik
force-pushed
the
feat/3201-slots-public-objects
branch
from
September 23, 2026 20:11
b16609a to
260c962
Compare
The second half of #3201: messages, responses and publish commands, routers with their `Route` and publisher objects, test clients and fakes, context, ASGI, the specification schema dataclasses and the telemetry/prometheus/acknowledgement middleware factories. The temporary `exclude-classes` group goes with it. `StreamMessage.__copy__` walks the slots, and `StreamMessage` declares `__weakref__` so a message can still key a WeakKeyDictionary. BREAKING CHANGE: these objects no longer accept attributes they do not declare; `message.custom = 1`, `Response(...).custom = 1` and `vars(message)` raise. A user's own subclass keeps a `__dict__`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On 3.10, `dataclass(slots=True)` re-emitted every inherited field into the subclass, so the chain under `BrokerConfig`, `EndpointConfig` and `SpecificationConfig` stayed unslotted. With 3.11 as the floor the 37 configs take `slots=True` and their `exclude-classes` entry goes. Below 3.13 a bare `super()` in a slotted dataclass still points at the class from before the rebuild, so the four calls in the Kafka (confluent) and MQTT broker configs name their class. `RabbitConfig` is gone: a mixin with its own fields cannot sit beside a slotted base, so `queue` and `exchange` are declared on the four Rabbit configs directly. BREAKING CHANGE: the configs no longer accept attributes they do not declare, and `faststream.rabbit.configs.RabbitConfig` no longer exists. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Lancetnik
force-pushed
the
feat/3201-slots-public-objects
branch
from
September 23, 2026 20:23
260c962 to
47468c6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The 1.0 half of #3201. The first half, #3220, is merged into
main.Important
The branch starts with a merge of
maininto1.0.0(3d9c5cb), because1.0.0does not have #3220 or the other 7 commitsmaingot since the last sync. Untilmainis merged into1.0.0, the diff shows those commits too. The PR's own changes are the two commits on top:1508ae0slots the public objects (below);260c962slots the config dataclasses, now that 1.0 requires Python 3.11+.Merge conflicts, all resolved to
1.0.0's version:_internal/fastapi/*(the plugin is gone on 1.0), the Redis connection state and producer (#3131 removed the sync cluster proxy and the separate cluster producer; #3220's__slots__stay on the classes that remain), andrequires-python/version inpyproject.toml.What this slots
144 classes, all of them objects that user code constructs, receives or holds:
StreamMessageand all 16 broker message classes.Response,PublishCommand,BatchPublishCommandand every broker's response/command.Registrator,BrokerRouter, every<Broker>Router,<Broker>Routeand<Broker>Publisher.TestApp,TestBroker, the sixTest<Broker>Brokerand their fakes.ContextRepo,Context.faststream/specification/schema/(Tag,Contact,License,ExternalDocs, the bindings), plusSchedule,ChannelandConsumerBuilder.<Broker>TelemetryMiddleware,<Broker>PrometheusMiddleware,AcknowledgementMiddleware. The per-message instances they create are already slotted in perf: make every declared __slots__ take effect in subclasses too #3220.It also removes the temporary
exclude-classesgroup that #3220 added for these classes.Changes beyond a
__slots__line:StreamMessage.__copy__copied throughself.__dict__. It now walks the class's slots, so a broker's own slots (KafkaMessage.consumer) are copied too, and a slot that was never assigned stays unset on the copy.StreamMessage.backgroundis a declared slot. The FastAPI plugin stores itsBackgroundTasksthere. When it is unset,getattr(msg, "background", None)still returnsNone.StreamMessagedeclares__weakref__, so a message can still key aWeakKeyDictionary.__copy__skips it.tests/message/test_message.py::test_message_takes_a_weak_referencefails without it.Config dataclasses (
260c962)#3220 left the config chain under
BrokerConfig,EndpointConfigandSpecificationConfigunslotted. On 3.10,dataclass(slots=True)re-emits every inherited field into the subclass, which doubles the storage instead of saving it. 1.0 requires 3.11+, so the 37 configs now takeslots=True, and theirexclude-classesentry is removed.super()in a slotted dataclass still points at the class from before the rebuild. The four such calls, in the Kafka (confluent) and MQTT broker configs, now name their class explicitly.RabbitConfigis removed. A mixin with its own fields cannot sit next to a slotted base.queueandexchangeare now declared on the four Rabbit configs directly.Breaking change
These objects no longer accept attributes they do not declare:
The same applies to the config dataclasses, and
faststream.rabbit.configs.RabbitConfigno longer exists.Not affected:
StreamMessage,Responseor a middleware keeps a__dict__.__dict__(perf: make every declared __slots__ take effect in subclasses too #3220), sopatch.object(...)on them still works.patch.object(broker.config, ...)goes through the unslottedConfigCompositionproxy, as before.Memory
Measured the same way as in #3220 (
sys.getsizeofof the object plus its__dict__):KafkaMessageKafkaResponseKafkaPublishCommandKafkaTelemetryMiddleware(factory)KafkaPrometheusMiddleware(factory)ChannelBinding(spec dataclass)Together with #3220, that is roughly 1.0 KB less per consumed message on a plain Kafka broker, and about 1.7 KB less with OpenTelemetry and Prometheus on. About 0.4 KB of that comes from this PR: the message and the publish command.
Checks
On this branch:
slotscheckon 3.11, 3.12, 3.13 and 3.14;mypy,pyright,ruff, and both collection gates; and thenot slow and not connectedsuite on 3.11 (3877 passed), 3.12 (3877 passed) and 3.14 (3853 passed). The in-memory suite covers thesuper()fix on 3.11 and 3.12. Connected suites were not run.