Skip to content

perf!: slot the public objects user code constructs, receives or holds - #3230

Merged
IvanKirpichnikov merged 4 commits into
1.0.0from
feat/3201-slots-public-objects
Oct 6, 2026
Merged

IvanKirpichnikov merged 4 commits into
1.0.0from
feat/3201-slots-public-objects

Conversation

@Lancetnik

@Lancetnik Lancetnik commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

The 1.0 half of #3201. The first half, #3220, is merged into main.

Important

The branch starts with a merge of main into 1.0.0 (3d9c5cb), because 1.0.0 does not have #3220 or the other 7 commits main got since the last sync. Until main is merged into 1.0.0, the diff shows those commits too. The PR's own changes are the two commits on top:

  • 1508ae0 slots the public objects (below);
  • 260c962 slots 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), and requires-python/version in pyproject.toml.

What this slots

144 classes, all of them objects that user code constructs, receives or holds:

  • Messages: StreamMessage and all 16 broker message classes.
  • Responses and publish commands: Response, PublishCommand, BatchPublishCommand and every broker's response/command.
  • Routers: Registrator, BrokerRouter, every <Broker>Router, <Broker>Route and <Broker>Publisher.
  • Test clients: TestApp, TestBroker, the six Test<Broker>Broker and their fakes.
  • Context: ContextRepo, Context.
  • ASGI: request, response, handlers, AsyncAPI route.
  • Specification schema: the dataclasses under faststream/specification/schema/ (Tag, Contact, License, ExternalDocs, the bindings), plus Schedule, Channel and ConsumerBuilder.
  • Middleware factories: <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-classes group that #3220 added for these classes.

Changes beyond a __slots__ line:

  • StreamMessage.__copy__ copied through self.__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.background is a declared slot. The FastAPI plugin stores its BackgroundTasks there. When it is unset, getattr(msg, "background", None) still returns None.
  • StreamMessage declares __weakref__, so a message can still key a WeakKeyDictionary. __copy__ skips it. tests/message/test_message.py::test_message_takes_a_weak_reference fails without it.

Config dataclasses (260c962)

#3220 left the config chain under BrokerConfig, EndpointConfig and SpecificationConfig unslotted. 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 take slots=True, and their exclude-classes entry is removed.

  • Below 3.13, a bare 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.
  • RabbitConfig is removed. A mixin with its own fields cannot sit next to a slotted base. queue and exchange are now declared on the four Rabbit configs directly.

Breaking change

These objects no longer accept attributes they do not declare:

message.custom = 1         # AttributeError
Response(b"").custom = 1   # AttributeError
vars(message)              # TypeError

The same applies to the config dataclasses, and faststream.rabbit.configs.RabbitConfig no longer exists.

Not affected:

  • A user's own subclass of StreamMessage, Response or a middleware keeps a __dict__.
  • The application, the broker, subscribers and publishers keep a __dict__ (perf: make every declared __slots__ take effect in subclasses too #3220), so patch.object(...) on them still works. patch.object(broker.config, ...) goes through the unslotted ConfigComposition proxy, as before.
  • Weak references to messages.

Memory

Measured the same way as in #3220 (sys.getsizeof of the object plus its __dict__):

class bytes before bytes after
KafkaMessage 344 176
KafkaResponse 344 72
KafkaPublishCommand 344 136
KafkaTelemetryMiddleware (factory) 376 64
KafkaPrometheusMiddleware (factory) 384 72
ChannelBinding (spec dataclass) 344 56

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: slotscheck on 3.11, 3.12, 3.13 and 3.14; mypy, pyright, ruff, and both collection gates; and the not slow and not connected suite on 3.11 (3877 passed), 3.12 (3877 passed) and 3.14 (3853 passed). The in-memory suite covers the super() fix on 3.11 and 3.12. Connected suites were not run.

@github-actions github-actions Bot added dependencies Pull requests that update a dependency file Confluent Issues related to `faststream.confluent` module AioKafka Issues related to `faststream.kafka` module NATS Issues related to `faststream.nats` module and NATS broker features Redis Issues related to `faststream.redis` module and Redis features MQTT Issues related to `faststream.mqtt` module AsyncAPI Issues related to AsyncAPI specification generation labels Sep 23, 2026
Base automatically changed from fix/3201-slots-subclasses to main September 23, 2026 19:49
@Lancetnik
Lancetnik changed the base branch from main to 1.0.0 September 23, 2026 20:01
@Lancetnik
Lancetnik force-pushed the feat/3201-slots-public-objects branch from b16609a to 260c962 Compare September 23, 2026 20:11
@github-actions github-actions Bot added documentation Improvements or additions to documentation Observability labels Sep 23, 2026
Lancetnik and others added 2 commits September 23, 2026 23:23
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
Lancetnik force-pushed the feat/3201-slots-public-objects branch from 260c962 to 47468c6 Compare September 23, 2026 20:23
@Lancetnik Lancetnik added the 1.0.0 Planned for the 1.0.0 release (may require breaking changes) label Sep 23, 2026
@IvanKirpichnikov
IvanKirpichnikov merged commit 74743ea into 1.0.0 Oct 6, 2026
29 checks passed
@IvanKirpichnikov
IvanKirpichnikov deleted the feat/3201-slots-public-objects branch October 6, 2026 11:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1.0.0 Planned for the 1.0.0 release (may require breaking changes) AioKafka Issues related to `faststream.kafka` module AsyncAPI Issues related to AsyncAPI specification generation Confluent Issues related to `faststream.confluent` module dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation MQTT Issues related to `faststream.mqtt` module NATS Issues related to `faststream.nats` module and NATS broker features Observability Redis Issues related to `faststream.redis` module and Redis features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants