Ordered set robust against mutation during iteration in Python
Project description
linkedset
An ordered set that is robust against mutation during iteration, implemented in pure Python.
DoublyLinkedSet behaves like an ordered set backed by a doubly linked list. Inserting
and removing elements is O(1), and — unlike Python's built-in list or set — you can
safely add, remove, or move elements while iterating over the container.
It implements both collections.abc.Sequence (ordered, indexable) and
collections.abc.MutableSet (set algebra and in-place updates).
📖 Documentation: https://justinchuby.github.io/linkedset/
Installation
pip install linkedset
Or install from source:
git clone https://github.com/justinchuby/linkedset
cd linkedset
pip install -e ".[dev]"
Usage
from linkedset import DoublyLinkedSet
s = DoublyLinkedSet(["a", "b", "c"])
s.append("d") # -> a, b, c, d
s.insert_after("a", ["x"]) # -> a, x, b, c, d
s.insert_before("b", ["y"]) # -> a, x, y, b, c, d
s.remove("c") # -> a, x, y, b, d
print(s[0]) # "a" (O(1))
print(s[-1]) # "d" (O(1))
print(list(s)) # ['a', 'x', 'y', 'b', 'd']
Safe mutation during iteration
s = DoublyLinkedSet(["a", "b", "c"])
for x in s:
if x == "a":
s.insert_after("a", ["d"]) # inserted after current -> iterated
s.remove("b") # removed before reached -> skipped
# Iterated: a, d, c
Iteration rules:
- Elements inserted after the current node are iterated over.
- Elements inserted before the current node are not iterated over in the current pass.
- If the current node is moved to a different location, iteration continues from the node that followed it at its original location.
Per-element mutation (add, remove, discard, append, insert, clear, …) is safe
during iteration. The global reorders reverse() and rotate() are not — calling them
mid-iteration may cause an in-progress iterator to skip or repeat elements.
Set operations
Because it is a MutableSet, the usual set algebra works and returns a new
DoublyLinkedSet (order preserved):
a = DoublyLinkedSet(["a", "b", "c"])
b = DoublyLinkedSet(["c", "d"])
a | b # union -> a, b, c, d
a & b # intersection -> c
a - b # difference -> a, b
a ^ b # symmetric -> a, b, d
a.add("x") # idempotent add (no-op if already present)
a.discard("z") # remove if present, never raises
a.pop() # remove and return the last element (list-style; pass an index too)
a |= b # in-place update
# `==` is order-sensitive, because the set is ordered:
DoublyLinkedSet(["a", "b"]) == DoublyLinkedSet(["a", "b"]) # True
DoublyLinkedSet(["a", "b"]) == DoublyLinkedSet(["b", "a"]) # False
Deque- and list-style methods
Because it is ordered, it also offers the familiar deque/list mutators (all keeping the
set's uniqueness and identity semantics):
s = DoublyLinkedSet(["a", "b", "c"])
s.appendleft("z") # -> z, a, b, c
s.extendleft(["x", "y"])# -> y, x, z, a, b, c (prepended, reversed like deque)
s.popleft() # removes and returns "y"
s.pop() # removes and returns the last element ("c")
s.pop(1) # removes and returns the element at index 1
s.insert(1, "q") # insert before index 1 (index clamped like list.insert)
s.rotate(1) # rotate right; negative rotates left
s.reverse() # reverse in place
s2 = s.copy() # shallow copy, order preserved
Semantics
- Membership and set operations are based on object identity (
id(value)), not equality. Two distinct objects that compare equal are treated as different elements. ==is order-sensitive (it is an ordered set): equal only when the same elements, by identity, appear in the same order. Instances are not hashable (mutable set). Ordering/subset comparisons (<,<=,>,>=) are not supported (they raiseTypeError), since a subset relation would be ambiguous next to order-sensitive equality; use the set algebra (&,|,-,^) orisdisjoint()instead.Noneis not a valid value.- Accessing by index is
O(n), except the ends (s[0],s[-1]) which areO(1).
Complexity
DoublyLinkedSet is a doubly linked list paired with a dict mapping each element's
id() to its list node. That combination gives set-like O(1) membership and endpoint
mutation, while preserving order and safe mutation during iteration.
Let n be the size of the set (and m the size of the other operand for binary set
operations).
| Operation | Complexity | Notes |
|---|---|---|
x in s, s.count(x) |
O(1) |
dict lookup by id(x) |
len(s) |
O(1) |
length is tracked, not counted |
s.append(x), s.add(x), s.appendleft(x) |
O(1) |
insert at a known end |
s.remove(x), s.discard(x) |
O(1) |
unlink the node, no shifting |
s.extend(xs), s.extendleft(xs) |
O(k) |
k = len(xs); O(1) per element |
s.insert_after(v, xs), s.insert_before(v, xs) |
O(k) |
k = len(xs); O(1) per element |
s.pop(), s.popleft() |
O(1) |
remove from an end |
s.pop(i), s.insert(i, x) |
O(|i|) |
walks to the index from the nearer end |
s[0], s[-1] |
O(1) |
endpoints are reachable directly |
s[i] |
O(|i|) |
walks from the nearer end |
s.index(x) |
O(n) |
linear scan for position |
s.rotate(n) |
O(n mod len(s)) |
relink only, no node churn |
s.reverse(), s.copy(), s.clear() |
O(n) |
|
iteration, reversed(s), s == other |
O(n) |
|
s[a:b] (slice) |
O(n) |
materialises a tuple |
s | t, s & t, s - t, s ^ t |
O(n + m) |
each membership test is O(1) |
s.isdisjoint(t) |
O(n) |
one pass with O(1) lookups |
Space is O(n): every element is wrapped in a small link node and referenced once from the
id-keyed index.
Mutating during iteration stays O(1) per operation. Removed nodes are unlinked from their
neighbours immediately, so a traversal never pays to skip over dead nodes — the only cost is
following the next pointer you already hold.
Development
pip install -e ".[dev]"
python -m pytest # run tests
python -m ruff check # lint
License
MIT. Portions derived from the ONNX Project Contributors (Apache-2.0); see linkedset/__init__.py.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file linkedset-0.1.0.tar.gz.
File metadata
- Download URL: linkedset-0.1.0.tar.gz
- Upload date:
- Size: 15.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da9dc8ad77a7574fce618debae72c7e732cc933bfe2ecd8e656a374f2ca3b983
|
|
| MD5 |
8d1a1c17f64a81f20e52b028a4578a32
|
|
| BLAKE2b-256 |
55f703c727b05cbe007567bf48e6a9e3bd3fa64e76e53b5d9cc665d5aee46878
|
Provenance
The following attestation bundles were made for linkedset-0.1.0.tar.gz:
Publisher:
publish.yml on justinchuby/linkedset
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
linkedset-0.1.0.tar.gz -
Subject digest:
da9dc8ad77a7574fce618debae72c7e732cc933bfe2ecd8e656a374f2ca3b983 - Sigstore transparency entry: 2054108559
- Sigstore integration time:
-
Permalink:
justinchuby/linkedset@2fc9e884b4f7b4afa1e0f5ddca9933537023d8a1 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/justinchuby
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2fc9e884b4f7b4afa1e0f5ddca9933537023d8a1 -
Trigger Event:
release
-
Statement type:
File details
Details for the file linkedset-0.1.0-py3-none-any.whl.
File metadata
- Download URL: linkedset-0.1.0-py3-none-any.whl
- Upload date:
- Size: 10.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e3dd0d00c04b60e683c4ce1f34e8217ef04bb95f2ceb5527fa288edb0e10dfa5
|
|
| MD5 |
6b8349b31cea205cb8d722483c44572d
|
|
| BLAKE2b-256 |
8c4e6acd9f53e1441b3e6549adf242f13fa76377cfb1fbe958abc695e55e2a1a
|
Provenance
The following attestation bundles were made for linkedset-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on justinchuby/linkedset
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
linkedset-0.1.0-py3-none-any.whl -
Subject digest:
e3dd0d00c04b60e683c4ce1f34e8217ef04bb95f2ceb5527fa288edb0e10dfa5 - Sigstore transparency entry: 2054108839
- Sigstore integration time:
-
Permalink:
justinchuby/linkedset@2fc9e884b4f7b4afa1e0f5ddca9933537023d8a1 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/justinchuby
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2fc9e884b4f7b4afa1e0f5ddca9933537023d8a1 -
Trigger Event:
release
-
Statement type: