CircularDict
A thread-safe Python dict that behaves like a circular buffer, evicting the oldest entries once a length or byte-size limit is reached.
Repository Health
Technical Analysis
CircularDict is a lightweight Python data structure that merges the familiar dict interface with the bounded-size behavior of a circular buffer. Built directly on top of collections.OrderedDict, it exposes every standard dictionary operation while enforcing either a maximum item count (maxlen), a maximum memory footprint in bytes (maxsize_bytes), or both simultaneously.
When a new item would push the dictionary past its configured limit, CircularDict automatically evicts the oldest entries (FIFO) until the constraint is satisfied again. All mutating operations are guarded by an RLock, making the class safe to share across threads. This makes it well suited to caching scenarios — such as buffering large NumPy arrays or other bulky objects — where an application needs predictable, bounded memory usage without hand-rolling its own eviction logic.
What You Get
- Drop-in dict replacement - subclasses
OrderedDict, so existing dict-based code keeps working withCircularDictswapped in. - Dual eviction limits - constrain by
maxlen(item count),maxsize_bytes(memory footprint), or both together. - Thread-safe mutations - every write path (
__setitem__,__delitem__,pop,popitem,clear) is guarded by an internalRLock. - PyPy compatibility fallback - detects when
sys.getsizeofisn’t fully supported (as on PyPy) and disables byte-size tracking gracefully instead of crashing.
Common Use Cases
- Bounded in-memory caches - cap a cache’s footprint without wiring up an external cache server.
- Sliding-window buffers for streaming data - keep only the most recent N readings or events in memory.
- Memory-constrained ML/data pipelines - buffer large NumPy arrays or tensors while enforcing a hard byte ceiling.
Under The Hood
Architecture
CircularDict keeps to a single, deliberately narrow surface: one class (CircularDict in circular_dict/CircularDict.py) that subclasses collections.OrderedDict and re-exports itself through circular_dict/__init__.py. There is no layering beyond this — construction validates that at least one of maxlen or maxsize_bytes is set, then every mutating entry point (__setitem__, __delitem__, pop, popitem, clear) is wrapped in an RLock and funnels through the same eviction logic: compute the incoming item’s byte size via sys.getsizeof, evict from the head of the ordered structure with popitem(last=False) until the byte or count ceiling is satisfied, then delegate to OrderedDict’s native implementation. The design explicitly works around a CPython 3.11 behavior change where pop/popitem stop calling __delitem__, detecting the discrepancy via a prev_size comparison and correcting current_size manually. Because the whole class is this one cohesive unit with no external collaborators, changing the core abstraction (say, replacing OrderedDict as the backing structure) would mean rewriting nearly the entire file, but the blast radius stops there — nothing else in the repo depends on internal details.
Tech Stack
The project has zero runtime dependencies beyond the Python standard library — collections.OrderedDict for ordered storage, threading.RLock for mutation safety, and sys.getsizeof for byte accounting (with a try/except fallback that no-ops size tracking on PyPy, where getsizeof isn’t fully implemented). Packaging uses a classic setup.py with setuptools.find_packages(), targeting python_requires>=3.6 and declaring Development Status :: 5 - Production/Stable in its PyPI classifiers; there is no pyproject.toml, no build-backend configuration, and no requirements.txt, since the library needs nothing beyond stdlib. The public API surface is deliberately just the one importable class, from circular_dict import CircularDict.
Code Quality
Testing lives entirely in a single top-level main_test.py that is executed as a plain script (if __name__ == '__main__':) rather than through pytest or unittest — assertions are hand-written assert statements covering maxlen eviction, maxsize_bytes eviction, deletion/pop/popitem/clear bookkeeping, and a 500-thread concurrency stress test, but there’s no test runner integration, no coverage tooling, and no CI workflow in .github/ (only a FUNDING.yml is present) — so tests are not automatically enforced on push or PR. Error handling is explicit and intentional where it matters (e.g. raising MemoryError when a single item alone exceeds maxsize_bytes), and every public method carries a docstring with typed parameter descriptions, but there are no mypy/ruff/flake8 configs, no type-checking in CI (since there is no CI), and the type hints, while present in signatures, aren’t verified anywhere.
API Design
The public API is a single class with two optional constructor parameters (maxlen, maxsize_bytes) and otherwise behaves exactly like a standard dict/OrderedDict, so there’s effectively zero boilerplate to start using it — CircularDict(maxlen=3) is the entire setup. Method names (is_empty, is_full, plus the inherited dict/OrderedDict surface) read naturally, and the combination of count-based and byte-based eviction in one structure is a genuinely useful, if narrow, capability — most circular-buffer or LRU-style libraries pick one dimension, not both. It isn’t architecturally novel (it’s a thin, well-executed wrapper over OrderedDict), but the dual-constraint eviction and the explicit CPython-version and PyPy compatibility handling reflect real production hardening rather than a toy implementation.