Skip to main content

PythonIota

A zero-dependency Python toolkit of from-scratch building blocks — Go-style iota enumerations, lazy sequence generators, and a Pike-VM regex engine.

pip install PythonIota

Requires Python 3.10+.

1.9.0

New features

  • Regex searches skip impossible starts using a conservative literal prefix; diagnostics expose the prefix and account for prefilter operations.
  • CLI mutually exclusive groups, version output and static completion scripts for Bash, Zsh and PowerShell extend the existing metadata-only completion API.
  • Sequence pipelines add bounded limit, grouping, Top-K, partitioning, iterator copies and combinatorics, with protection for known non-exhausting inputs.

Fixes

  • Pattern backreferences now fail explicitly instead of silently matching digits.
  • Out-of-range regex positions are clamped to the text, matching re semantics.
  • Counted nullable repetitions stop after zero-width iterations without losing the preferred non-empty alternative during iteration.

1.8.0

New features

  • Generic, single-pass IotaIterator pipelines support continuous chaining.
  • CLI options support explicit JSON/mapping configuration, opt-in environment values and metadata-only completion candidates.
  • Regex operations support shared step/time budgets and structured diagnostics.

Fixes

  • Mapping derived sequences preserves their lazy iteration, element types and finite/infinite behavior; filter still returns an iterator.
  • Empty regex matches, conditional capture states and zero-width repetitions retain the appropriate alternatives and iteration order.
  • Parent CLI positionals are validated before subcommand dispatch.
  • Enum aliases cannot overwrite existing or reserved names; alias identifiers must already be NFKC-normalized to agree with Python attribute syntax.
  • Sequence APIs carry generic element types, and distributions include py.typed.

1.7.1 fixes

  • Full matching now considers complete alternatives instead of rejecting a valid match after selecting a shorter branch.
  • Ordered enum members now hash consistently with equal numeric values and equal members of other enums, preserving dictionary and set lookups.

What's inside

  • Enums — Go-style iota, bit flags, string enums (pythoniota.IotaEnum, ...)
  • Sequences — lazy, chainable generators (pythoniota.iota)
  • Regex — a from-scratch NFA (Pike VM) engine, no catastrophic backtracking (pythoniota.regex)
  • CLI — a from-scratch argparse-style argument parser (pythoniota.cli)

Enums

Go-style iota

iota starts at 0 and auto-increments each time it is read. Literal assignments do not consume it.

from pythoniota import IotaEnum

class Color(IotaEnum):
    Red   = iota   # 0
    Green = iota   # 1
    Blue  = iota   # 2

Color.Green          # 1
Color.names()        # ['Red', 'Green', 'Blue']
Color.values()       # [0, 1, 2]
list(Color)          # [('Red', 0), ('Green', 1), ('Blue', 2)]
0 in Color           # True (by value)
'Red' in Color       # True (by name)
Color.from_value(2)  # 'Blue'

iota works inside expressions — the value is the current counter:

class Perm(IotaEnum):
    Read  = 1 << iota          # 1
    Write = 1 << iota          # 2
    Exec  = 1 << iota          # 4
    All   = Read | Write | Exec # 7

class Size(IotaEnum):
    KB = 1 << (iota + 10)      # 1024
    MB = 1 << (iota + 10)      # 2048
    GB = 1 << (iota + 10)      # 4096

Skipping values & custom start/step

class Errno(IotaEnum):
    EPERM = iota   # 0
    skip(2)        # skip 1, 2
    EBADF = iota   # 3

class Port(IotaEnum):
    _iota_start_ = 8000
    _iota_step_  = 10
    HTTP  = iota   # 8000
    HTTPS = iota   # 8010

_ = iota also skips a single value (Go's blank identifier).

Immutability, aliases, serialization

Color.Red = 99          # AttributeError: cannot modify enum member
Color.alias('R', 'Red') # Color.R == 0
Color.to_dict()         # {'Red': 0, 'Green': 1, 'Blue': 2}
Color.to_json()         # '{"Red": 0, "Green": 1, "Blue": 2}'
Color.from_json(s)      # -> dict of members
Color.has('Red')        # True
Color.get('Nope', -1)   # -1

Alias names must be public Python identifiers. Existing members, aliases, methods and reserved/internal names cannot be overwritten; rejected aliases leave the enum unchanged.

@unique — reject duplicate values

from pythoniota import unique

@unique
class Status(IotaEnum):
    Active   = iota
    Inactive = iota
# raises ValueError if two members share a value

Ordered enums

Add _ordered_ = True to make members comparable by declaration order:

class Priority(IotaEnum):
    _ordered_ = True
    Low    = iota
    Medium = iota
    High   = iota

Priority.Low < Priority.High   # True

Bit flags

from pythoniota import IotaBitFlags, FlagScope

class Access(IotaBitFlags):
    Read  = 1 << iota   # 1
    Write = 1 << iota   # 2
    Exec  = 1 << iota   # 4

flags = Access.Read | Access.Write
flags.has(Access.Read)              # True
flags.has_all(Access.Read, Access.Exec)  # False
flags.has_any(Access.Read, Access.Exec)  # True
list(flags.decompose())             # [BitFlag(1), BitFlag(2)]

with FlagScope() as scope:
    scope.grant(Access.Read, Access.Write)
    scope.revoke(Access.Write)
    scope.has(Access.Read)          # True

String enums

Members resolve to their own name, or a custom format:

from pythoniota import IotaStringEnum

class Color(IotaStringEnum):
    Red  = iota   # 'Red'
    Blue = iota   # 'Blue'

class Code(IotaStringEnum):
    _format_ = "{name}_{index:03d}"
    OK    = iota  # 'OK_000'
    Error = iota  # 'Error_001'

@iota_enum decorator

Turn a plain class into an enum:

from pythoniota import iota_enum

@iota_enum
class Color:
    Red = 0
    Green = 1
    Blue = 2

@iota_enum(ordered=True)
class Priority:
    Low = 0
    High = 1

match / case

Enum members match by value:

match color:
    case Color.Red:   ...
    case Color.Blue:  ...
    case _:           ...

Sequences

iota(...) is a lazy, composable sequence. Construction mirrors range, plus an optional map:

from pythoniota import iota

iota()                       # 0, 1, 2, ...        (infinite)
iota(5)                      # 0, 1, 2, 3, 4
iota(2, 10, 2)               # 2, 4, 6, 8
iota(5, map=lambda i: i*i)   # 0, 1, 4, 9, 16

s = iota(10)
len(s)          # 10        (O(1))
s[7]            # 7         (O(1) arithmetic indexing)
s[2:5]          # [2, 3, 4]
list(reversed(s))   # 9, 8, ... 0   (lazy)
5 in s          # True      (O(1))
s.take(3)       # [0, 1, 2]

Operators

iota(3) + iota(3, 6)   # concat: 0,1,2,3,4,5
iota(3) * 2            # repeat: 0,1,2,0,1,2
iota(3) | iota(3, 6)   # interleave: 0,3,1,4,2,5
iota(3) @ iota(3, 6)   # zip: (0,3),(1,4),(2,5)

Lazy combinators

iota(10).filter(lambda x: x % 2 == 0)   # 0,2,4,6,8
iota().map(lambda x: x*x)               # 0,1,4,9,...
iota().takewhile(lambda x: x < 5)       # 0,1,2,3,4
iota(10).dropwhile(lambda x: x < 7)     # 7,8,9
iota(4).pairwise()                      # (0,1),(1,2),(2,3)
iota(5).window(3)                       # (0,1,2),(1,2,3),(2,3,4)
iota(7).chunk(3)                        # [0,1,2],[3,4,5],[6]
iota(6).map(lambda x: x % 3).distinct() # 0,1,2
seq.flatten()                           # flatten one level
iota().enumerate()  .accumulate()  .zip(other)

Chainable iterator pipelines

from pythoniota import IotaIterator, iota

windows = iota(20).filter(lambda x: x % 2 == 0).map(str).window(3)
windows.take(2)  # [('0', '2', '4'), ('2', '4', '6')]
next(windows)   # ('4', '6', '8'): continues the same stream
IotaIterator([1, 2, 3]).map(lambda x: x * 10).to_list()  # [10, 20, 30]
iota().filter(lambda x: x % 2 == 0).map(str).take(3)    # ['0', '2', '4']

filter, accumulate, enumerate, zip, chunk, window, takewhile, dropwhile, pairwise, flatten and distinct return IotaIterator pipelines, which can continue chaining. They are lazy and single-pass (iter(pipe) is pipe), with element types preserved through generic transformations. take(n) still returns a list. Chunk/window sizes must be positive and are checked before consumption.

Base sequences and their .map() results remain reusable. Iterator pipelines have no length, but expose infinite: True for known non-exhausting sources, False for known finite sources, and None when unknown. Exhaustive terminals reject known non-exhausting pipelines before consuming them. Filtering/distinct can yield few values without ever exhausting their infinite input; these retain the protection. Arbitrary generators and early-stopping takewhile may remain unknown, so this is not a general termination detector.

Use .limit(n) to keep a bounded prefix chainable (take(n) still returns a list):

iota().filter(lambda x: x % 2 == 0).limit(5).sum()  # 20

limit bounds the number of returned elements, not the time to produce one: iota().filter(lambda x: False).limit(1) still cannot yield a value. Short-circuit operations (first, find, any, all, take) remain available on infinite input.

Grouping, selection and iterator utilities

iota(6).group_by(lambda x: x % 2)  # {0: [0, 2, 4], 1: [1, 3, 5]}
iota(10).top_k(3)                 # [9, 8, 7]
iota(10).top_k(3, largest=False)  # [0, 1, 2]
iota(2).zip_longest(["a"], fillvalue="-").to_list()  # [(0, 'a'), (1, '-')]
iota(2).product(["a", "b"]).to_list()  # [(0, 'a'), (0, 'b'), (1, 'a'), (1, 'b')]
iota(3).permutations(2).take(3)   # [(0, 1), (0, 2), (1, 0)]

odds, evens = iota(6).partition(lambda x: x % 2 == 0)
odds.to_list(), evens.to_list()  # ([1, 3, 5], [0, 2, 4])
left, right = iota(3).tee()
left.take(2), right.to_list()    # ([0, 1], [0, 1, 2])

group_by(key=None) groups globally into lists, preserving key/element insertion order; unlike itertools.groupby, equal keys need not be adjacent. top_k supports key=, preserves input order for ties and keeps O(k) selection storage, but must consume the entire finite input. top_k(0) returns immediately without consuming. Neither operation can compute a global result from an infinite stream.

partition returns false/true branches and evaluates its predicate once per item. Like tee, it buffers when consumers advance at different rates; that buffer can grow without bound. product and permutations defer input consumption until iteration, then cache their inputs like itertools; they reject known infinite inputs and are not constant-memory streaming operations.

Terminal operations

Reductions on plain arithmetic sequences are O(1); known-infinite base sequences reject exhaustive terminals. Iterator pipelines consume their remaining elements.

iota(1, 101).sum()      # 5050   (closed-form, O(1))
iota(3, 10).min()       # 3
iota(3, 10).max()       # 9
iota(0, 10, 2).count()  # 5
iota(5).last()          # 4
iota(10).nth(3)         # 3
iota().first()          # 0
iota().find(lambda x: x > 100)   # 101
iota(1, 5).all(lambda x: x > 0)  # True
iota(5).any(lambda x: x == 3)    # True
iota(3).reduce(lambda a, b: a + b)  # 3

iota(3).to_list()                     # [0, 1, 2]
iota(3).to_tuple()                    # (0, 1, 2)
iota(3).to_set()                      # {0, 1, 2}
iota(3).to_dict(value=lambda x: x*x)  # {0: 0, 1: 1, 2: 4}

Async iteration

async for x in iota(5):
    ...

Recipes

from pythoniota.recipes import (
    fibonacci, lucas, factorial, catalan, harmonic,
    triangle, powers, geometric, primes, primes_sieve,
    pascal_row, collatz, repeat, cycle,
)

fibonacci(10).to_list()   # 0,1,1,2,3,5,8,13,21,34
lucas(7).to_list()        # 2,1,3,4,7,11,18
factorial(6).to_list()    # 1,1,2,6,24,120
catalan(6).to_list()      # 1,1,2,5,14,42
harmonic(4).to_list()     # 1.0, 1.5, 1.833..., 2.083...
triangle(5).to_list()     # 0,1,3,6,10
powers(2, 8).to_list()    # 1,2,4,...,128
geometric(3, 2, 4).to_list()  # 3,6,12,24
primes(5).to_list()       # 2,3,5,7,11        (first n primes)
primes_sieve(20).to_list()# 2,3,5,7,11,13,17,19  (< limit, Eratosthenes)
pascal_row(4).to_list()   # 1,4,6,4,1
collatz(6).to_list()      # 6,3,10,5,16,8,4,2,1
repeat('x', 3).to_list()  # ['x','x','x']
cycle([1,2], 5).to_list() # 1,2,1,2,1

Recipes without a count argument produce infinite sequences: fibonacci().take(20), primes().takewhile(lambda p: p < 100).


Safe expression evaluation

safe_eval evaluates arithmetic expressions over a whitelisted AST (no eval, no names/calls beyond provided variables):

from pythoniota import safe_eval

safe_eval("2 ** 10")                 # 1024
safe_eval("a * b + 1", {"a": 3, "b": 4})  # 13

Regex

A from-scratch regular-expression engine that runs bytecode with an ordered Pike VM. Plain patterns such as (a+)+$ avoid catastrophic backtracking and run in linear time in the text length for a fixed pattern. This is not a universal complexity guarantee: lookaround can rescan suffixes, and conditionals can retain multiple capture-participation states. Use explicit max_steps and timeout limits when matching potentially expensive patterns.

from pythoniota import regex

m = regex.match(r"(\d{4})-(\d{2})-(\d{2})", "2026-08-02")
m.groups()                                   # ('2026', '08', '02')

regex.findall(r"\w+", "hello world")          # ['hello', 'world']
regex.sub(r"\s+", "_", "a  b   c")            # 'a_b_c'
regex.split(r"[,;]\s*", "a, b; c,d")          # ['a', 'b', 'c', 'd']

named = regex.search(r"(?P<user>\w+)@(?P<host>\w+)", "user@host")
named.groupdict()                             # {'user': 'user', 'host': 'host'}

Supported: literals, ., * + ? and their lazy forms *? +? ??, alternation |, groups (...) / non-capturing (?:...) / named (?P<name>...), lookahead (?=...) / (?!...), fixed-width lookbehind (?<=...) / (?<!...), conditionals (?(id/name)yes|no), character classes [...], counted repeats {m,n}, shorthands \d \w \s (and \D \W \S), word boundaries \b \B, anchors ^ $ \A \Z, and hex escapes \xHH / \uHHHH. Flags: IGNORECASE, MULTILINE, DOTALL, VERBOSE (also inline (?imsx)). API mirrors re: compile, match, fullmatch, search, findall, finditer, sub, subn, split, escape, with Match.group / groups / span / groupdict / expand.

regex.findall(r"[a-z]+", "AbCdEf", regex.IGNORECASE)   # ['AbCdEf']
regex.findall(r"\d+(?=px)", "10px 20em 30px")           # ['10', '30']  (lookahead)

Pattern backreferences (\1, (?P=name)) raise RegexError; this is an NFA-based engine rather than a general backtracking engine. Replacement templates still support backreferences. Patterns are text strings, not bytes. Empty matches follow the iteration and splitting behavior of Python 3.10–3.13 re, including a non-empty match immediately after an empty one at the same position.

regex.search(r"(a+)+$", "a" * 40 + "!")       # None, without backtracking

Execution limits and diagnostics

pattern = regex.compile(r"(?=a*$)")
try:
    result = pattern.search("a" * 100 + "!", max_steps=1000, timeout=0.1)
except regex.RegexLimitError as error:
    print(error.reason, error.steps)

structure = pattern.explain()  # JSON-serializable bytecode, groups and nested assertions
report = pattern.diagnose("aaaa!", max_steps=1000)
print(report["status"], report["steps"], report["op_counts"])

Search and iteration use a conservative mandatory literal prefix when available. explain()["literal_prefix"] shows it; case-insensitive patterns and leading branching/nullable patterns fall back to the normal VM. Existing higher-priority threads keep running: the optimization does not restart the VM for each candidate.

All matching, iteration, splitting and replacement functions accept keyword-only max_steps (nonnegative integer) and timeout (nonnegative seconds), defaulting to no limits. A step is a bytecode analysis/visit or one native prefix lookup (PREFILTER), including epsilon closure and consuming states; counts are implementation-specific, not a portable time unit or a per-character work bound. Nested lookarounds and all matches within one call share a single budget. finditer starts its timeout when created, including pauses between yields.

Limit exhaustion raises RegexLimitError, never a false no-match result. diagnose supports match, fullmatch and search, instead returning an explicit limit_exceeded status with its reason, counters, peak pending threads and elapsed time. explain and diagnose also exist as module-level functions.

Limits apply after pattern compilation. Timeouts are cooperative: they cannot preempt Python replacement callbacks, compilation or a single long native operation; elapsed time is checked again after callbacks. For hard isolation of untrusted patterns/callbacks, use a separate process and external resource limits.


CLI

A faithful subset of the standard library's argparse, built from scratch:

from pythoniota import cli

p = cli.ArgumentParser(prog="greet", description="say hello")
p.add_argument("name")
p.add_argument("-n", "--times", type=int, default=1)
p.add_argument("-v", "--verbose", action="store_true")

args = p.parse_args(["world", "--times", "3", "-v"])
args.name, args.times, args.verbose      # ('world', 3, True)

Supported:

  • positional & optional arguments, nargs (N / ? / * / +)
  • type / choices / default / required
  • actions: store, store_true, store_false, store_const, append, count
  • --opt=value, clustered short flags (-abc), the -- end-of-options marker
  • an auto-generated -h/--help, and sub-commands via add_subparsers

Bad input prints a usage line and raises SystemExit(2) — the same contract as argparse, so it drops into scripts and tests unchanged. parse_known_args returns (namespace, extras) instead of erroring on unknown tokens.

Mutually exclusive options and version output

p = cli.ArgumentParser(prog="convert")
mode = p.add_mutually_exclusive_group(required=True)
mode.add_argument("--json", action="store_true")
mode.add_argument("--text", action="store_true")
p.add_argument("--version", action="version", version="%(prog)s 1.0")
p.parse_args(["--json"])

Groups accept optional arguments only. A required group needs exactly one explicit member; an optional group permits zero or one. CLI, environment and configuration values count as explicit, even False or zero; defaults and caller Namespace values do not. CLI overrides lower sources for the same destination, not another group member: CLI --json plus config text=true is a conflict. Repeating the same member is allowed. Group members cannot themselves be required or share a destination.

A valid version request prints to stdout and exits with status 0 without reading configuration or converting unrelated values. It bypasses missing required options but still respects argument consumption, subcommand boundaries and --.

Sub-commands

p = cli.ArgumentParser(prog="git")
sub = p.add_subparsers(dest="cmd")

add = sub.add_parser("add", help="stage files")
add.add_argument("path")
add.add_argument("-f", "--force", action="store_true")

rm = sub.add_parser("rm", help="remove files")
rm.add_argument("path")

args = p.parse_args(["add", "README.md", "-f"])
args.cmd, args.path, args.force          # ('add', 'README.md', True)

Parent positionals declared before add_subparsers are consumed and validated before the command. Parent options may occur before the command; trailing options are parsed only by the selected child. As with argparse, variable nargs are greedy and command names are not special delimiters, so prefer fixed-count parent positionals for predictable subcommand syntax.

Configuration and environment values

p = cli.ArgumentParser(config={"port": 8000, "mode": "fast"}, env_prefix="APP_")
p.add_argument("--port", type=int, default=3000)
p.add_argument("--mode", choices=["fast", "slow"])
p.add_argument("--token", env_var="SERVICE_TOKEN")
args = p.parse_args(["--port", "9000"])  # --port overrides APP_PORT and config

config accepts a mapping or a UTF-8 JSON-object path, read again on each parse. Keys are this parser's optional-argument destinations, not option spellings; unknown keys and malformed files are errors. Sources do not fill positionals. Precedence is explicit CLI > enabled environment > config > argument default; only the winning value is converted and checked against choices. Required options may be supplied by config/environment. Explicit caller Namespace values are retained unless the CLI changes them, but do not by themselves satisfy required options.

Environment lookup is opt-in via env_prefix or an exact env_var; children configure their own sources with add_parser(config=..., env_prefix=...). Scalar environment values are literal strings. Multi-value options use JSON arrays; append uses an array of occurrences (nested arrays for multi-value occurrences). Boolean values accept true/false, yes/no, on/off and 1/0; counts must be nonnegative integers. Explicit CLI append/count replaces lower sources, while legacy accumulation from argument defaults is retained when no external source applies. No configuration contents are executed as code or shell text.

Completion candidates

p.complete([], "--po")             # ['--port']
p.complete(["--mode"], "s")         # ['slow']
p.complete([], "--mode=f")         # ['--mode=fast']

complete(tokens, prefix="") accepts finished words without the program/current word, then returns sorted, unique full replacement words for options, choices and subcommands. It does not parse, read config/environment, call type converters, execute shells or mutate parse state. Variable-length positionals are best-effort; only built-in collections of plain scalar choices are enumerated, not generators or custom objects. This is an adapter API for shells/editors: it does not install shell hooks, quote candidates or perform filesystem completion.

Static shell completion scripts

script = p.completion_script("bash", command="convert")
# Also accepts "zsh" and "powershell"; save/load the returned text yourself.

This generates a quoted static snapshot of options, subcommands and finite scalar choices. It does not run the application, read sources, call converters or change shell configuration. Unlike complete(tokens, prefix), the snapshot is not a context-sensitive parser: it may suggest candidates from other options or commands. Regenerate it after changing parser metadata. Shell metacharacters are emitted as literal data, not executable fragments. Command names containing control characters are rejected; such candidates are omitted. Zsh requires an initialized completion system (compinit).


License

MIT

Metadata

Release files for PythonIota 1.12.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for PythonIota 1.12.0
File Size Uploaded Provenance Actions
pythoniota-1.12.0.tar.gz 131.0 kB Without provenance Details for pythoniota-1.12.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for PythonIota 1.12.0
File Interpreter ABI Platform Actions
pythoniota-1.12.0-py3-none-any.whl Python 3 none any Details for pythoniota-1.12.0-py3-none-any.whl

Total release size: 203.8 kB

Release files / pythoniota-1.12.0.tar.gz

Download URL pythoniota-1.12.0.tar.gz
Size 131.0 kB
Tags Source
SHA-256 checksum
How to use checksums
e8e67bcb7bc5bcd9baa4dadc659e6aff74dd5d444acea930133f90aeb33c7954
BLAKE2b-256 checksum
How to use checksums
2f6dbf8871ffaf9d14108e72058f3ba778e4e7dbc97c103b7a3963422711494f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.9

Release files / pythoniota-1.12.0-py3-none-any.whl

Download URL pythoniota-1.12.0-py3-none-any.whl
Size 72.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
71ed34fb1e2d80dbfdf4173b5e7aa1d377fd175835473babc14116d034d93cf7
BLAKE2b-256 checksum
How to use checksums
70de1cc3dae256d5e7c52d2d9207d54f93c7d93d6d7e210dbed78a78cbc46cdf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.9

Security for PythonIota 1.12.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page