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
resemantics. - 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
IotaIteratorpipelines 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;
filterstill 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 viaadd_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.10.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pythoniota-1.10.0.tar.gz | 110.2 kB | Without provenance | Details for pythoniota-1.10.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pythoniota-1.10.0-py3-none-any.whl | Python 3 | none | any | Details for pythoniota-1.10.0-py3-none-any.whl |
Total release size: 164.8 kB
Release files / pythoniota-1.10.0.tar.gz
| Download URL | pythoniota-1.10.0.tar.gz |
|---|---|
| Size | 110.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2479445b622ed13ee502cec2225331c8d707cd92f6bb21dd6caefd2970ed01b5
|
|
BLAKE2b-256 checksum How to use checksums |
a2a440365bdb6eb5b6145e124b37b5a03280b5b4f06c9b5ff06ae80716dab4fd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.9
|
Release files / pythoniota-1.10.0-py3-none-any.whl
| Download URL | pythoniota-1.10.0-py3-none-any.whl |
|---|---|
| Size | 54.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4c90206cb607f0d530c7c8d7a6cc4a920c2bb710718983ba275dc716e43b96dc
|
|
BLAKE2b-256 checksum How to use checksums |
25a85632d10e52e241771287b516396f0af89423a9003cd22c04fca56023ae87
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.9
|
Security for PythonIota 1.10.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.