On this page
- A Text Box That Needs to Mean Five Different Things
- What You Need to Know First
- A Post Office Sorting Machine
- Chunk 1: The Entry Point and Dispatch Table
- Chunk 2: The Regex-Gated INT Parser
- Chunk 3: Float Ranges With Decimal Arithmetic
- Chunk 4: The Seed Parser Delegates Instead of Duplicating
- Edge Cases the Parser Handles Without Crashing
A Text Box That Needs to Mean Five Different Things
You have a text box. A user types "10-30, 5" and expects the Python list [10, 15, 20, 25, 30]. Another user types "0.5-1.5, 0.25" and expects [0.5, 0.75, 1.0, 1.25, 1.5]. A third types "euler, dpm_2, ddim" and expects a list of strings. A fourth types "random:5" and expects five randomly generated integers.
Same input widget. Five completely different types. Completely different parsing logic for each.
The naive solution — one function with a forest of if/elif branches — becomes unmaintainable the moment you add a sixth type. It’s also untestable: you can’t isolate one parser from another, which means a bug in float parsing can theoretically interfere with seed parsing if you’re not careful.
This tutorial dissects core/parameter_parser.py, a self-contained module that handles all five parameter types (INT, FLOAT, STRING, COMBO, SEED) through a dispatch table pattern. Every parser is a standalone function. The entry point is a single parse() function that routes to the right one based on a type key.
What You Need to Know First
- Python functions,
str.split(),str.strip() - Regular expressions at a basic level (
re.match, capture groups) - Python type hints (
list[int],list[float]) - What a dispatch table is (a
dictmapping keys to callables)
Python >= 3.9
Standard library only: re, decimal, random
You don’t need ComfyUI installed to follow this. The module is fully standalone.
A Post Office Sorting Machine
parse() is the intake slot. Every string comes through one entrance. The machine reads the label (param_type) and sends the string down the correct chute. Each chute has a specialist at the end. The specialists don’t know about each other; the sorting machine doesn’t know how to parse — it only knows how to route.
flowchart TD
A["parse(raw_string, param_type)"] --> B{Lookup in dispatch dict}
B -->|"INT"| C[parse_int]
B -->|"FLOAT"| D[parse_float]
B -->|"STRING"| E[parse_string]
B -->|"COMBO"| F[parse_combo]
B -->|"SEED"| G[parse_seed]
B -->|"Unknown"| H["raise ValueError"]
C --> C1{Range regex match?}
C1 -->|Yes| C2["range(start, end+1, step)"]
C1 -->|No| C3["split(',') → int()"]
D --> D1{Range regex match?}
D1 -->|Yes| D2["Decimal arithmetic loop"]
D1 -->|No| D3["split(',') → float()"]
G --> G1{Prefix match?}
G1 -->|"random:N"| G2["[randint() × N]"]
G1 -->|"increment:start:count"| G3["range(start, start+count)"]
G1 -->|"Neither"| G4["delegate to parse_int"]
Adding a new type means adding one key-value pair to the dict and one new function. Nothing else changes.
Chunk 1: The Entry Point and Dispatch Table
import random
import re
from decimal import Decimal, ROUND_HALF_UP
def parse(raw_string: str, param_type: str) -> list:
"""Main entry point. Dispatches to type-specific parsers."""
if not raw_string or not raw_string.strip():
return []
dispatch = {
"INT": parse_int,
"FLOAT": parse_float,
"STRING": parse_string,
"COMBO": parse_combo,
"SEED": parse_seed,
}
parser = dispatch.get(param_type.upper())
if parser is None:
raise ValueError(f"Unknown parameter type: {param_type}")
return parser(raw_string.strip())
The dispatch table stores references to functions, not calls to them — parse_int (no parentheses) stores the function object. When you write parser(raw_string.strip()), you’re calling it. This is first-class function behavior: Python treats functions as values, just like integers or strings. The .upper() call makes the lookup case-insensitive, so "int" and "INT" both work.
Chunk 2: The Regex-Gated INT Parser
Here’s what most beginners write first:
# Fragile: doesn't handle ranges, gives terrible error messages
def parse_int_naive(raw):
return [int(x.strip()) for x in raw.split(",")]
This breaks on "10-30, 5" with a cryptic ValueError: invalid literal for int() with base 10: '10-30'. The user gets no guidance on what format is expected.
Here’s what the repo actually does:
def parse_int(raw: str) -> list[int]:
raw = raw.strip()
range_match = re.match(
r"^(-?\d+)\s*-\s*(-?\d+)(?:\s*,\s*(\d+))?$", raw
)
if range_match:
start = int(range_match.group(1))
end = int(range_match.group(2))
step = int(range_match.group(3)) if range_match.group(3) else 1
if step <= 0:
raise ValueError(f"Step must be positive, got {step}")
if start <= end:
return list(range(start, end + 1, step))
else:
return list(range(start, end - 1, -step))
# Not a range: fall through to comma-separated list
values = []
for part in raw.split(","):
part = part.strip()
if part:
try:
values.append(int(part))
except ValueError:
raise ValueError(
f"Cannot parse '{part}' as integer. "
f"Use comma-separated values (10, 20, 30) "
f"or range syntax (10-30, 5)"
)
return values
The regex acts as a gate. If the input looks like a range (10-30, 5), use range logic. If not, fall through to list logic. The two branches are mutually exclusive and independently testable.
The error message in the except clause is the important part — it tells the user exactly what formats are accepted. Generic ValueError messages are useless to users who don’t know your parsing rules.
Chunk 3: Float Ranges With Decimal Arithmetic
Try this in your REPL:
>>> 0.1 + 0.2
0.30000000000000004
If you build a float range using native arithmetic, you accumulate drift. By the fourth step, 1.0000000000000002 appears in your list, which breaks equality checks and produces ugly filenames for generated images.
def parse_float(raw: str) -> list[float]:
raw = raw.strip()
range_match = re.match(
r"^(-?[\d.]+)\s*-\s*(-?[\d.]+)(?:\s*,\s*([\d.]+))?$", raw
)
if range_match:
# Construct Decimal from string, NOT from float.
# Decimal(0.1) inherits float's imprecision.
# Decimal("0.1") is exact.
start = Decimal(range_match.group(1))
end = Decimal(range_match.group(2))
step = Decimal(range_match.group(3)) if range_match.group(3) else Decimal("1")
values = []
current = start
while current <= end + step / Decimal("1000"):
precision = max(
-start.as_tuple().exponent if start.as_tuple().exponent < 0 else 0,
-step.as_tuple().exponent if step.as_tuple().exponent < 0 else 0,
)
rounded = float(
current.quantize(
Decimal("0." + "0" * precision),
rounding=ROUND_HALF_UP,
)
)
values.append(rounded)
current += step # Decimal addition: exact
return values
Decimal stores numbers as coefficient × 10^exponent — the same representation humans use. Decimal arithmetic is 10–100x slower than float, but this parser runs once at job-submission time to generate a list of at most a few hundred values. The performance cost is completely negligible; the correctness benefit is not.
The step / Decimal("1000") tolerance in the while condition prevents off-by-one errors where the loop exits one step too early due to accumulated boundary rounding.
Chunk 4: The Seed Parser Delegates Instead of Duplicating
The SEED parser is notable because it doesn’t reimplement integer list parsing — it calls parse_int when neither seed shorthand syntax matches:
def parse_seed(raw: str) -> list[int]:
raw = raw.strip()
# "random:5" → five random seeds
random_match = re.match(r"^random\s*:\s*(\d+)$", raw, re.IGNORECASE)
if random_match:
count = int(random_match.group(1))
if count <= 0:
raise ValueError("Random seed count must be positive")
if count > 10000:
raise ValueError("Random seed count too large (max 10000)")
return [random.randint(0, 2**32 - 1) for _ in range(count)]
# "increment:100:5" → [100, 101, 102, 103, 104]
inc_match = re.match(
r"^increment\s*:\s*(\d+)\s*:\s*(\d+)$", raw, re.IGNORECASE
)
if inc_match:
start = int(inc_match.group(1))
count = int(inc_match.group(2))
if count <= 0:
raise ValueError("Increment count must be positive")
return list(range(start, start + count))
# Fallback: treat as plain integer list
return parse_int(raw)
This is parser composition: parse_seed is built on top of parse_int. The shared fallback path eliminates duplicated comma-list logic. If parse_int gets a bug fix, parse_seed benefits automatically.
The seed count cap at 10,000 exists because "random:100000" would queue 100,000 images. That’s a mistake, not a feature. The error message makes the limit explicit.
One thing worth noting: random.randint uses Python’s Mersenne Twister PRNG, which is not cryptographically secure. For generative AI seeds, that’s completely fine — you want varied but reproducible-ish seeds, not secrets. Don’t use this function for anything security-sensitive.
Edge Cases the Parser Handles Without Crashing
"-5-5" is ambiguous. Is it start=-5, end=5 or something else? The regex ^(-?\d+)\s*-\s*(-?\d+) handles it correctly: parse_int("-5-5") → [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5]. The optional leading minus on the first capture group and the whitespace-tolerant separator in the middle make this unambiguous.
"10,,20" produces ["10", "", "20"] after split(","). The if part: guard skips empty strings. So "10,,20" and "10, 20" produce the same result — the parser doesn’t punish typos with cryptic errors.
The parser functions have no shared state and no side effects beyond their return values. They’re trivially thread-safe: calling parse() from 50 concurrent threads produces correct results from all 50 without any locking.
The dispatch table pattern replaces if/elif chains with a dictionary of callables. You’ll see it everywhere in production Python: Django’s URL router, Python’s ast.NodeVisitor, virtually every plugin system. Recognizing when a branching problem is actually a routing problem is a sign of intermediate-to-senior Python fluency.