
A Pythonic class for sequences considered circular —
where the element after the last wraps back to the first.
RingSeqPy is a small, zero-dependency library exposing a single generic
RingSeq[T] class that wraps any Iterable[T], stores it internally as an
immutable tuple, and implements the Python Sequence protocol circularly.
Instances are hashable, orderable, and interoperable: RingSeq('ABC'),
RingSeq(['A','B','C']), and RingSeq(('A','B','C')) all compare equal.
pip install ring-seq-py
Working for Python 3.10 and above.
>>> from ring_seq import RingSeq
>>> # Indexing wraps around
>>> RingSeq([10, 20, 30])[4]
20
>>> # Slicing wraps around too; transformations return a new RingSeq,
>>> # unwrap with .to_list() / .to_tuple() / .to_str()
>>> RingSeq([0, 1, 2]).rotate_right(1).to_list()
[2, 0, 1]
>>> # Comparison up to rotation
>>> RingSeq([0, 1, 2]).is_rotation_of([2, 0, 1])
True
>>> # Canonical (necklace) form for deduplication
>>> RingSeq([2, 0, 1]).canonical().to_list()
[0, 1, 2]
>>> # Symmetry detection
>>> RingSeq([0, 1, 0, 1]).rotational_symmetry()
2
>>> # Strings work naturally; to_str() rejoins
>>> RingSeq('RING').rotate_right(1).to_str()
'GRIN'
| Method | Description |
|---|
rs[i] | Element at circular index (any integer wraps) |
rs[i:j], rs[i:j:k] | Circular slice (can exceed ring length; negative step traverses backward) |
len(rs), iter(rs), reversed(rs), x in rs | Standard protocol, no surprises |
rs == other, hash(rs), min(rings) | Positional equality; lexicographic ordering |
get(i, default=None) | Like rs[i], but returns default on an empty ring |
index(value, start=0, stop=None) | Circular first-occurrence lookup |
index_of_slice(that, from_=0) | Circular index of a contiguous (possibly wrapping) slice, or None |
contains_slice(that) | Whether a slice occurs in the ring, wrapping included |
| Method | Description |
|---|
to_list() | Return a new list |
to_tuple() | Return the internal tuple |
to_str(sep='') | Join elements into a str |
| Method | Description |
|---|
index_from(i) | Normalize a circular index to [0, len) |
| Method | Description |
|---|
rotate_right(step) | Rotate right by step (negative = left) |
rotate_left(step) | Rotate left by step (negative = right) |
start_at(i) | Rotate so circular index i is first |
reflect_at(i=0) | Reflect so circular index i is the axis head |
| Method | Description |
|---|
take_while(p, from_=0) | Longest prefix from from_ satisfying p |
drop_while(p, from_=0) | Remainder after that prefix |
span(p, from_=0) | (take_while, drop_while) in one call |
| Method | Description |
|---|
rotations() | All n rotations (lazy) |
reflections() | Original + reflection (lazy) |
reversions() | Original + reversal (lazy) |
rotations_and_reflections() | All 2n variants (lazy) |
windows(size) | n sliding windows of fixed size, wrapping the seam |
grouped(size) | ceil(n / size) fixed-size blocks, last one wraps the seam |
zip_with_index(from_=0) | Elements paired with their circular indices |
| Method | Description |
|---|
is_rotation_of(that) | Same elements, possibly rotated? |
is_reflection_of(that) | Same elements, possibly reflected? |
is_reversion_of(that) | Same elements, possibly reversed? |
is_rotation_or_reflection_of(that) | Either of the above? |
align_to(that) | k such that start_at(k) == that, or None |
hamming_distance(that) | Positional mismatches (same size required) |
min_rotational_hamming_distance(that) | Minimum distance over all rotations |
| Method | Description |
|---|
canonical_index() | Index of lex-smallest rotation (two-pointer minimal rotation, O(n) time, O(1) space) |
canonical() | Lex-smallest rotation (necklace form) |
bracelet() | Lex-smallest under rotation and reflection |
| Method | Description |
|---|
rotational_symmetry() | Order of rotational symmetry |
symmetry_indices() | Shifts where the ring equals its reversal rotated left |
reflectional_symmetry_axes() | Full axis geometry (Vertex / Edge pairs) |
symmetry() | Number of reflectional symmetry axes |
RingSeq subclasses collections.abc.Sequence, so native Python protocols
do the work where they map cleanly — rs[i] for indexing, rs[i:j] for
slicing, x in rs for containment. A few methods are deliberately renamed
from the Scala/Rust counterparts to be Pythonic:
| This library | Elsewhere |
|---|
rs[i] | apply (Rust), applyO (Scala) |
rs[i:j], rs[i:j:k] | slice (Rust), sliceO (Scala) |
windows | windows (Rust), slidingO (Scala) |
grouped | chunks (Rust) |
zip_with_index | enumerate (Rust) |
RingSeq.index(value, start=0, stop=None) overrides Sequence.index with
circular semantics: without a stop it searches one full revolution and
returns an index in [0, len).
- Bioinformatics — circular DNA/RNA sequence alignment and comparison
- Graphics — polygon vertex manipulation, closed curve operations
- Procedural generation — tile rings, symmetry-aware pattern generation
- Music theory — pitch-class sets, chord inversions
- Combinatorics — necklace/bracelet enumeration, Burnside's lemma
- Embedded / robotics — circular sensor arrays, rotary encoder positions
The same library, adapted for the specific idiom, is available also for:
Licensed under either of
at your option.