Skip to content

flyweight

Intern instances by their constructor arguments, so equal arguments always yield the same object.

When to reach for it

Reach for flyweight when many parts of your program ask for the same logical thing and you want them to share one immutable instance. Colors, currencies, glyphs, units, parsed schemas, anything that is value-like and expensive or wasteful to rebuild. The same arguments give you the same object, built once and shared forever.

The anti-pattern it replaces

It replaces @cache slapped over __init__, which does not actually work, since __init__ returns None and the cache caches nothing useful while the object is still rebuilt. It also replaces ad-hoc interning dicts scattered across the codebase. flyweight moves interning into the type, keeps instances immutable, and builds each one exactly once.

Usage

FlyweightMeta is a metaclass. Set it on the class you want interned, and construction caches by the constructor arguments.

from patos import FlyweightMeta


class Color(metaclass=FlyweightMeta):
    def __init__(self, name: str) -> None:
        self.name = name


a = Color("teal")
b = Color("teal")
c = Color("amber")

assert a is b       # same args, same interned object
assert a is not c

Because FlyweightMeta composes with ABCMeta, a flyweight can also be abstract.

from abc import abstractmethod
from patos import FlyweightMeta


class Shape(metaclass=FlyweightMeta):
    @abstractmethod
    def area(self) -> float: ...

Public API

  • FlyweightMeta. The metaclass that performs interning. Use it as class X(metaclass=FlyweightMeta). The first construction with a given (args, kwargs) builds and caches the instance, and every later construction with equal arguments returns that same object without re-running __init__. Each class keeps its own cache and arguments must be hashable. It composes with ABCMeta, so a flyweight can also be an abstract base class. An argument whose equality is not a plain bool (a model carrying tensors, where a == b reduces a tensor and raises) is interned by identity rather than value, so the same object still shares one instance while two equal-but-distinct such arguments get separate instances rather than crashing the lookup.

Source

Copy this into your project and own it. No dependency, no tool, just one module you can read and change.

from abc import ABCMeta
from typing import TypeVar, cast

_ResultT = TypeVar("_ResultT")

# Named rather than written inline as `except (RuntimeError, ValueError, TypeError):`, because
# ruff targets 3.14 for the `sql` extra and its formatter rewrites an inline tuple into PEP 758's
# unparenthesized form, which the 3.13 core floor cannot parse. A single name is the one spelling
# both interpreters accept and no formatter rewrites. These are what an argument raises when its
# `==` refuses to answer, a tensor-carrying model among them.
_UNRESOLVABLE_EQUALITY = (RuntimeError, ValueError, TypeError)


class Arg:
    """One flyweight key element: hashes by its argument and compares without a tensor `__eq__`.

    The flyweight interns by argument value, but an argument whose `==` does not return a plain
    `bool` -- a pydantic model carrying tensors reduces a tensor to a bool and raises, a raw tensor
    returns an elementwise mask -- cannot key a plain `dict`, whose lookup would raise on the
    collision compare. Resolving a hash collision by identity first, then a guarded value compare
    that treats an unresolvable equality as distinct, keeps interning exact for well-behaved
    arguments and degrades to per-object for one whose equality is undefined, never crashing.
    Typing stays like `lru_cache(typed=True)`: the argument's type joins the hash and gates the
    compare, so `Arg(1)`, `Arg(True)` and `Arg(1.0)` are distinct keys.
    """

    __slots__ = ("kind", "value")

    def __init__(self, value: object) -> None:
        self.kind = type(value)
        self.value = value

    def __hash__(self) -> int:
        return hash((self.kind, self.value))

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Arg) or self.kind is not other.kind:
            return False
        if self.value is other.value:
            return True
        try:
            return self.value == other.value
        except _UNRESOLVABLE_EQUALITY:
            return False


CacheKey = tuple[tuple[Arg, ...], frozenset[tuple[str, Arg]]]


class FlyweightMeta(ABCMeta):
    """Metaclass that interns instances by their construction arguments.

    The first construction with a given `(args, kwargs)` builds and caches the instance; every
    later construction with equal arguments returns that same object without re-running
    `__init__` -- the metaclass owns `__call__`, so no re-entrancy guard is needed (unlike
    caching `__new__`, which still re-runs `__init__`). Each class keeps its own cache and
    arguments must be hashable. Interning is typed like `lru_cache(typed=True)`, so `Node(1)`,
    `Node(True)` and `Node(1.0)` stay distinct instances even though the arguments compare
    equal. Subclasses `ABCMeta` so a flyweight can also be an abstract base or carry abstract
    methods.
    """

    # `cls: type[_ResultT]` makes `Node(...)` return `Node`. Construction args stay `object`
    # because one metaclass serves every class, each with its own `__init__` signature.
    def __call__(cls: type[_ResultT], *args: object, **kwargs: object) -> _ResultT:
        cache: dict[CacheKey, object] = cls.__dict__.get("flyweights", {})
        if "flyweights" not in cls.__dict__:
            type.__setattr__(cls, "flyweights", cache)
        key = (
            tuple(map(Arg, args)),
            frozenset((name, Arg(value)) for name, value in kwargs.items()),
        )
        if key not in cache:
            cache[key] = type.__call__(cls, *args, **kwargs)
        return cast(_ResultT, cache[key])