{
  "type": "new-runtime-knowledge-guide",
  "version": 1,
  "generated_at": "2026-07-23",
  "volume": 20,
  "slug": "python-llm-utilities-reference",
  "title": "The Python Utility Stack for LLM Pipelines",
  "description": "LLM applications live not only on models. Around them there is almost always templating, structure validation, string sanitization, JSON parsing, regex, file paths, batching, caching, async I/O and little standard library functions that save hours. This volume is not about “AI”, but about service Python, which makes the LLM pipeline stable and readable.",
  "track": {
    "slug": "application-engineering",
    "title": "Application Engineering",
    "route": "https://newruntime.com/learn/tracks/application-engineering/",
    "position": 2
  },
  "counts": {
    "sections": 7,
    "cards": 37
  },
  "routes": {
    "html": "https://newruntime.com/learn/python-llm-utilities-reference/",
    "json": "https://newruntime.com/learn/python-llm-utilities-reference.json"
  },
  "sections": [
    {
      "number": "01",
      "title": "What Is This Layer About?",
      "intro": "",
      "cards": [
        {
          "number": "01",
          "title": "LLM application = model + service Python around it",
          "tag": "Framing",
          "description": "In practice, most of the code around LLM does not generate tokens, but prepares the data for the model call and parses the result afterwards. This is where prompt templates, schema validation, string cleanup, JSON parsing, file/path logic, retries, batching and tracing live.",
          "blocks": [
            {
              "kind": "code",
              "label": "Diagram",
              "language": "text",
              "value": "TYPICAL NON-MODEL LAYER\n\ninput text\n  ↓\nnormalize / strip / parse\n  ↓\nrender prompt template\n  ↓\ncall model\n  ↓\nparse JSON / validate schema\n  ↓\nlog/retry/cache/save\n\nOften it is this layer that determines the reliability of the system more than the choice of model"
            }
          ]
        },
        {
          "number": "02",
          "title": "Four Repetitive Tasks Around LLM",
          "tag": "Framing",
          "description": "Almost all “helper libraries” around LLM solve one of four problems: text rendering, structure checking, data transformation and orchestration runtime. If you keep this in mind, it is easier to choose a tool without the cult of libraries.",
          "blocks": [
            {
              "kind": "code",
              "label": "four classes of problems",
              "language": "text",
              "value": "1.render:\n   collect string/prompt/message\n\n2.validate:\n   make sure the structure is correct\n\n3. transform:\n   clean, parse, serialize, cut\n\n4. orchestrate:\n   batch, cache, run async, retrace"
            }
          ]
        },
        {
          "number": "03",
          "title": "Taking the library too early is also a mistake",
          "tag": "Framing",
          "description": "Not every task requires Jinja or Pydantic. Sometimes `f-string`, `dict`, `json.loads` and a couple of string methods are enough. A library is justified when it provides repeatability, safety, or readability better than bare Python.",
          "blocks": [
            {
              "kind": "list",
              "label": "good rule",
              "items": [
                "one small prompt → f-string",
                "a lot of templates and branches → Jinja",
                "external JSON / API contract → Pydantic",
                "drag the library for one line `.strip()`"
              ]
            }
          ]
        },
        {
          "number": "04",
          "title": "Mental model: entrances are dirty, exits are fragile",
          "tag": "Framing",
          "description": "LLM pipelines almost always stand between a non-ideal input and a non-ideal output. The user text is dirty, the retrieval context is noisy, the model output is unstable. Therefore, service Python is needed as a layer of normalization and insurance on both sides.",
          "blocks": [
            {
              "kind": "code",
              "label": "basic logic",
              "language": "text",
              "value": "dirty input\n→ make predictable\n→ give to the model\n→ get fragile output\n→ make it safe and structured"
            }
          ]
        }
      ]
    },
    {
      "number": "02",
      "title": "Templating: From f-String to Jinja",
      "intro": "",
      "cards": [
        {
          "number": "05",
          "title": "f-string, `.format()`, Jinja are three different levels",
          "tag": "Templating",
          "description": "For short prompts, `f-string` is usually sufficient. If reuse and a little more explicit naming is needed, `.format()` is sometimes suitable. When loops, conditions, blocks and external patterns appear on disk, the Jinja zone begins.",
          "blocks": [
            {
              "kind": "table",
              "headers": [
                "Tool",
                "Good for",
                "Bad for"
              ],
              "rows": [
                [
                  "f-string",
                  "short inline template",
                  "complex conditions and loops"
                ],
                [
                  "`.format()`",
                  "named placeholders",
                  "large prompt trees"
                ],
                [
                  "Jinja",
                  "many templates, loops, if, reuse",
                  "overkill for one line"
                ]
              ]
            }
          ]
        },
        {
          "number": "06",
          "title": "Jinja starts with `Environment`",
          "tag": "Templating",
          "description": "In Jinja, the central object is `Environment`. It stores loader, filters, globals, undefined behavior and template cache. For applications, this means that it is better to initialize the template engine once at the start of the service, rather than creating a new `Template(...)` for each request.",
          "blocks": [
            {
              "kind": "code",
              "label": "typical setup",
              "language": "sql",
              "value": "from jinja2 import Environment, PackageLoader, StrictUndefined\n\nenv = Environment(\n    loader=PackageLoader(\"app\"),\n    undefined=StrictUndefined,\n)\n\ntemplate = env.get_template(\"answer_prompt.j2\")\nprompt = template.render(query=query, context=context)"
            }
          ]
        },
        {
          "number": "07",
          "title": "`StrictUndefined` is more useful for prompts than it seems",
          "tag": "Templating",
          "description": "By default, the template may silently swallow the missing variable and render empty. For HTML this is already unpleasant, for prompt it is even worse: you will silently lose part of the instruction or context. Therefore, for LLM templates it is often wise to use `StrictUndefined` and fail early.",
          "blocks": [
            {
              "kind": "list",
              "label": "typical use",
              "items": [
                "catches missing `context`",
                "catches typos in keys",
                "better fail fast than silently wrong prompt"
              ]
            }
          ]
        },
        {
          "number": "08",
          "title": "Jinja is especially good when the prompt has loops and conditions",
          "tag": "Templating",
          "description": "As soon as the prompt says “insert all retrieved chunks”, “if there is history - show it”, “if debug mode - add trace”, string concatenation begins to fall apart. Jinja makes these branches explicit and readable.",
          "blocks": [
            {
              "kind": "code",
              "label": "typical pattern",
              "language": "text",
              "value": "{% if history %}\nConversation history:\n{% for msg in history %}\n- {{ msg }}\n{% endfor %}\n{% endif %}\n\nQuestion:\n{{ query }}\n\n{% for item in context_items %}\n[{{ item.label }}]\n{{ item.text }}\n{% endfor %}"
            }
          ]
        },
        {
          "number": "09",
          "title": "When Jinja is not needed",
          "tag": "Templating",
          "description": "If the template is small, lives close to the code, and has no logic, `f-string` is usually simpler. Jinja makes sense when templates become a separate resource, require reuse, or are used by different teams and modes.",
          "blocks": [
            {
              "kind": "code",
              "label": "rule of thumb",
              "language": "python",
              "value": "if prompt_fits_on_screen and has_no_loops:\n    use f-string\nelse:\n    use Jinja"
            }
          ]
        }
      ]
    },
    {
      "number": "03",
      "title": "Schema, Typing and Pydantic",
      "intro": "",
      "cards": [
        {
          "number": "10",
          "title": "Typing, dataclasses and Pydantic are not the same thing",
          "tag": "Schema",
          "description": "`typing` describes the expected form of the data. `dataclass` conveniently creates lightweight Python objects. `Pydantic` adds runtime validation, coercion, serialization and JSON schema generation. In LLM pipelines, most often `typing` specifies a contract, and `Pydantic` makes it executable on dirty external input/output.",
          "blocks": [
            {
              "kind": "table",
              "headers": [
                "Layer",
                "What it does it give?",
                "When to take"
              ],
              "rows": [
                [
                  "`typing`",
                  "annotations and contracts",
                  "always"
                ],
                [
                  "`dataclass`",
                  "light domain objects",
                  "internal structures without dirty input"
                ],
                [
                  "`Pydantic`",
                  "validation + parsing + dump",
                  "API, model output, config, external data"
                ]
              ]
            }
          ]
        },
        {
          "number": "11",
          "title": "`BaseModel` - the main entry point to Pydantic",
          "tag": "Schema",
          "description": "The main value of `BaseModel` is that it takes imperfect data and tries to force it into the expected form. This is especially useful for model output, webhook payloads, retrieval traces and structured response contracts.",
          "blocks": [
            {
              "kind": "code",
              "label": "basic example",
              "language": "python",
              "value": "class Answer(BaseModel):\n    answer: str\n    citations: list[str]\n    confidence: float\n\nobj = Answer.model_validate(payload)"
            }
          ]
        },
        {
          "number": "12",
          "title": "Three very useful methods: `model_validate`, `model_validate_json`, `model_dump`",
          "tag": "Schema",
          "description": "In LLM pipelines, these three methods cover the lion's share of needs. `model_validate()` - when you already have a dict. `model_validate_json()` - when model output came as a JSON string. `model_dump()` - when you need to send the structure further to the log, API or prompt.",
          "blocks": [
            {
              "kind": "code",
              "label": "three modes",
              "language": "text",
              "value": "Answer.model_validate({\"answer\": \"...\", \"citations\": []})\nAnswer.model_validate_json(raw_json_string)\nanswer_obj.model_dump()"
            }
          ]
        },
        {
          "number": "13",
          "title": "Validators are needed where “just types” are no longer sufficient",
          "tag": "Schema",
          "description": "In real code, it is often not enough to say “the field must be a string.” You also need to check that `citations` are not empty, that `score` is in the range, that the two fields are consistent, that the Markdown is cleaned, and that the JSON does not contain forbidden keys. This is where `field_validator` and `model_validator` come in.",
          "blocks": [
            {
              "kind": "code",
              "label": "typical use case",
              "language": "python",
              "value": "class Answer(BaseModel):\n    answer: str\n    citations: list[str]\n\n    @field_validator(\"answer\")\n    @classmethod\n    def clean_answer(cls, value: str) -> str:\n        return value.strip()\n\n    @model_validator(mode=\"after\")\n    def ensure_citations(self):\n        if not self.citations:\n            raise ValueError(\"citations required\")\n        return self"
            }
          ]
        },
        {
          "number": "14",
          "title": "When is `dataclass` better than Pydantic",
          "tag": "Schema",
          "description": "If the data is already trusted and you just want a clean lightweight object for internal calculations, `dataclass` is often better. It's simpler, lighter, and faster for domain objects that don't require runtime validation on every input.",
          "blocks": [
            {
              "kind": "list",
              "label": "good signal in favor of dataclass",
              "items": [
                "the object is created only from already validated data",
                "need plain Python carrier object",
                "I don’t want to drag validation into a hot internal loop"
              ]
            }
          ]
        },
        {
          "number": "15",
          "title": "`typing.Protocol`, `Literal`, `TypedDict` are often underestimated",
          "tag": "Schema",
          "description": "Even without Pydantic, typing helps LLM code a lot. `Protocol` allows you to describe adapter interfaces, `Literal` fixes valid modes, `TypedDict` is convenient for intermediate JSON-like structures where you don’t want full-fledged objects yet.",
          "blocks": [
            {
              "kind": "code",
              "label": "example",
              "language": "python",
              "value": "class Embedder(Protocol):\n    def embed_query(self, text: str) -> list[float]: ...\n\nMode = Literal[\"chat\", \"rag\", \"judge\"]"
            }
          ]
        }
      ]
    },
    {
      "number": "04",
      "title": "Text Munging: Small Inline Functions with Big Effect",
      "intro": "",
      "cards": [
        {
          "number": "16",
          "title": "`strip`, `split`, `splitlines`, `join` - four basic workhorses",
          "tag": "Text",
          "description": "A lot of LLM code comes down to neatly assembling and disassembling strings. `strip()` removes excess noise at the edges, `split()` cuts along delimiters, `splitlines()` is useful for line-based parsing responses, `join()` puts text back together without ugly concatenation.",
          "blocks": [
            {
              "kind": "code",
              "label": "frequent pattern",
              "language": "python",
              "value": "text = raw_text.strip()\nlines = text.splitlines()\nlines = [line.strip() for line in lines if line.strip()]\nnormalized = \"\\n\".join(lines)"
            }
          ]
        },
        {
          "number": "17",
          "title": "`partition()` is often more convenient than `split(..., 1)`",
          "tag": "Text",
          "description": "When you need to split a string once into a before/delimiter/after, `partition()` gives a stable triple result and doesn't make you wonder if the delimiter was found. For LLM pipelines, this is useful when parsing headers, source labels, Markdown front matter, and prefixes like `Answer:`.",
          "blocks": [
            {
              "kind": "code",
              "label": "example",
              "language": "python",
              "value": "prefix, sep, rest = raw.partition(\"Answer:\")\n\nif sep:\n    answer = rest.strip()\nelse:\n    answer = raw.strip()"
            }
          ]
        },
        {
          "number": "18",
          "title": "`removeprefix` and `removesuffix` are safer than `replace`",
          "tag": "Text",
          "description": "If you need to remove a specific prefix or suffix, it is better to use special methods rather than `replace()`. Otherwise, you can unexpectedly delete the desired fragment somewhere inside the line. This is especially noticeable when working with marks like `json\\n...`, `assistant:` and markdown fences.",
          "blocks": [
            {
              "kind": "code",
              "label": "example",
              "language": "text",
              "value": "raw = raw.removeprefix(\"```json\").removesuffix(\"```\")\nraw = raw.strip()"
            }
          ]
        },
        {
          "number": "19",
          "title": "`textwrap.dedent()` saves multi-line prompts",
          "tag": "Text",
          "description": "When prompt is written in triple quotes inside Python code, there is almost always extra indentation. `textwrap.dedent()` makes the template visually beautiful in code and at the same time removes the overall leading whitespace before sending it to the model.",
          "blocks": [
            {
              "kind": "code",
              "label": "typical use case",
              "language": "text",
              "value": "prompt = dedent(\"\"\"\n    You are a helpful assistant.\n    Return valid JSON.\n    Question: {query}\n\"\"\").strip()"
            }
          ]
        },
        {
          "number": "20",
          "title": "`re` is needed not for “understanding the text”, but for targeted cleaning and extraction",
          "tag": "Text",
          "description": "Regular expressions in LLM code are useful for limited, formal things: extracting fenced JSON, replacing extra spaces, finding citation labels, normalizing bullet markers. They are bad for complex semantic parsing, but great for dirty mechanics.",
          "blocks": [
            {
              "kind": "code",
              "label": "typical operations",
              "language": "text",
              "value": "json_block = re.search(r\"```json\\s*(.*?)\\s*```\", raw, re.S)\ncitations = re.findall(r\"\\[SOURCE\\s+\\d+\\]\", raw)\nclean = re.sub(r\"\\n{3,}\", \"\\n\\n\", raw)"
            },
            {
              "kind": "list",
              "label": "important rule",
              "items": [
                "regex for format",
                "regex for complex JSON structure",
                "write raw strings: `r\\\"...\\\"`"
              ]
            }
          ]
        },
        {
          "number": "21",
          "title": "`replace()` is good, as long as replacement is really stupid",
          "tag": "Text",
          "description": "`replace()` is great for banal normalization: replace `\\r\\n` with `\\n`, remove non-breaking spaces, bring Unicode quotes to the same style. But as soon as the replacement has context and conditions, it is better to move on to regex or explicit parsing.",
          "blocks": [
            {
              "kind": "code",
              "label": "simple substitutions",
              "language": "text",
              "value": "text = text.replace(\"\\r\\n\", \"\\n\")\ntext = text.replace(\"\\u00a0\", \" \")\ntext = text.replace(\"—\", \"-\")"
            }
          ]
        },
        {
          "number": "22",
          "title": "List comprehensions + `join()` are often better than complex loops",
          "tag": "Text",
          "description": "Many small preparatory steps can be expressed in one readable line: clear the list of chunks, discard empty ones, add labels and assemble the final context block. This makes prompt-building shorter and more transparent.",
          "blocks": [
            {
              "kind": "code",
              "label": "example",
              "language": "python",
              "value": "context_block = \"\\n\\n\".join(\n    f\"[SOURCE {i}] {item.text.strip()}\"\n    for i, item in enumerate(context_items, start=1)\n    if item.text.strip()\n)"
            }
          ]
        }
      ]
    },
    {
      "number": "05",
      "title": "JSON, Files, Paths And Storage Media",
      "intro": "",
      "cards": [
        {
          "number": "23",
          "title": "`json.loads` / `json.dumps` - almost mandatory layer around structured output",
          "tag": "Data",
          "description": "Even if the model promises to “return JSON,” the code still needs an explicit parse step. `json.loads()` converts string into Python structures, `json.dumps()` - vice versa. It's useful to remember that parsing untrusted JSON can be expensive, meaning input size and parsing errors need to be controlled.",
          "blocks": [
            {
              "kind": "code",
              "label": "typical pattern",
              "language": "python",
              "value": "try:\n    data = json.loads(raw_model_output)\nexcept json.JSONDecodeError:\n    return fallback_or_retry()\n\nvalidated = ResponseModel.model_validate(data)"
            }
          ]
        },
        {
          "number": "24",
          "title": "`pathlib.Path` makes file code cleaner",
          "tag": "Data",
          "description": "For prompt files, evaluation datasets, cached traces, and local documents, `Path` is usually more convenient than manually working with path strings. Plus it is easier to combine with glob search and cross-platform logic.",
          "blocks": [
            {
              "kind": "code",
              "label": "example",
              "language": "sql",
              "value": "from pathlib import Path\n\nprompt_dir = Path(\"prompts\")\ntemplate_path = prompt_dir / \"judge.j2\"\ntext = template_path.read_text(encoding=\"utf-8\")"
            }
          ]
        },
        {
          "number": "25",
          "title": "CSV is still useful for eval sets and batch inference",
          "tag": "Data",
          "description": "There is a surprising amount of CSV around LLM pipelines: offline evaluation, manual markup, log export, FAQ import, list of golden queries. The standard `csv` module can already do quite a lot and does not require dragging pandas if the task is small.",
          "blocks": [
            {
              "kind": "code",
              "label": "frequent use-case",
              "language": "text",
              "value": "with open(\"eval.csv\", newline=\"\", encoding=\"utf-8\") as f:\n    reader = csv.DictReader(f)\n    rows = list(reader)"
            }
          ]
        },
        {
          "number": "26",
          "title": "Serialization is part of the contract, not a technical detail",
          "tag": "Data",
          "description": "As soon as trace, chunks, answers or prompts are written to a file or log, you already live in a world of format conventions. Therefore, it is useful to decide in advance whether you have line-delimited JSON, a regular JSON array, CSV or plain text logs, rather than writing “whatever happens.”",
          "blocks": [
            {
              "kind": "list",
              "label": "simple landmark",
              "items": [
                "JSONL for traces/events",
                "CSV for manual tables and eval",
                "plain text for prompt snapshots",
                "format should be fixed early"
              ]
            }
          ]
        },
        {
          "number": "27",
          "title": "`dict.get()` and `setdefault()` are often a rescue from brittle code",
          "tag": "Data",
          "description": "Not every intermediate structure in a pipeline requires a full model. When you're working with half-baked dicts from retrieval payloads, API responses, or trace blocks, `get()` and `setdefault()` make the code smoother and reduce the noise from checking for keys.",
          "blocks": [
            {
              "kind": "code",
              "label": "example",
              "language": "text",
              "value": "source = payload.get(\"source_url\")\nmeta = payload.setdefault(\"metadata\", {})\nmeta.setdefault(\"tenant_id\", tenant_id)"
            }
          ]
        }
      ]
    },
    {
      "number": "06",
      "title": "Pipeline Utilities: Collections, Itertools, Cache, Async",
      "intro": "",
      "cards": [
        {
          "number": "28",
          "title": "`defaultdict` and `Counter` - small but very useful",
          "tag": "Pipeline",
          "description": "LLM pipelines constantly group something and count: hits by `doc_id`, errors by type, citation coverage, usage stats, retry reasons. For this, `defaultdict` and `Counter` often produce more readable code than manual `if key not in dict` and increments.",
          "blocks": [
            {
              "kind": "code",
              "label": "example",
              "language": "python",
              "value": "by_doc = defaultdict(list)\nfor hit in hits:\n    by_doc[hit.doc_id].append(hit)\n\nerror_counts = Counter(trace.reason for trace in traces)"
            }
          ]
        },
        {
          "number": "29",
          "title": "`itertools.batched`, `chain`, `islice` help with batch flow",
          "tag": "Pipeline",
          "description": "When you need to process documents into embedding batches, lazily merge multiple data streams, or take only the first N elements without extra copies, `itertools` come in very handy. In LLM code, this is especially common in ingestion and bulk evaluation.",
          "blocks": [
            {
              "kind": "code",
              "label": "typical use cases",
              "language": "python",
              "value": "for batch in batched(chunks, 32):\n    vectors = embedder.embed_documents([c.text for c in batch])\n\nall_queries = chain(user_queries, golden_queries)\npreview = list(islice(all_queries, 10))"
            }
          ]
        },
        {
          "number": "30",
          "title": "`functools.cache` / `lru_cache` are useful for deterministic helpers",
          "tag": "Pipeline",
          "description": "A cache based on pure helper functions sometimes gives unexpected benefits: tokenization of repeating pieces, reading templates from disk, normalizing directories, calculating small derived settings. But the cache is poorly suited for functions with side effects, async generators, and things that depend on rapidly changing external state.",
          "blocks": [
            {
              "kind": "code",
              "label": "example",
              "language": "python",
              "value": "@lru_cache(maxsize=128)\ndef load_prompt(name: str) -> str:\n    return (Path(\"prompts\") / name).read_text(encoding=\"utf-8\")"
            }
          ]
        },
        {
          "number": "31",
          "title": "`functools.partial` works well for preset configurations",
          "tag": "Pipeline",
          "description": "When the same helper is called with almost the same arguments, `partial()` helps to capture some of the parameters and make the code shorter. This can be convenient for render helpers, logger wrappers and small adapters over SDK calls.",
          "blocks": [
            {
              "kind": "code",
              "label": "example",
              "language": "text",
              "value": "render_judge = partial(render_prompt, mode=\"judge\")\nrender_rag = partial(render_prompt, mode=\"rag\")"
            }
          ]
        },
        {
          "number": "32",
          "title": "`asyncio` is needed where the pipeline runs into I/O",
          "tag": "Pipeline",
          "description": "Many LLM systems are network-based in nature: external LLM API, vector DB, reranker API, object storage, telemetry backend. Therefore, `asyncio` and `async/await` are often useful not for “AI magic”, but for normal control of parallel I/O. Especially in FastAPI and batch-evaluation pipelines.",
          "blocks": [
            {
              "kind": "code",
              "label": "frequent pattern",
              "language": "typescript",
              "value": "results = await asyncio.gather(\n    retrieve(query1),\n    retrieve(query2),\n    retrieve(query3),\n)\n\nusually useful in conjunction with semaphore/rate limiting"
            }
          ]
        },
        {
          "number": "33",
          "title": "Batching is not only about models, but also about Python code",
          "tag": "Pipeline",
          "description": "If you have 1000 documents for embeddings or 500 eval queries, a naive piece-by-piece loop will often be too slow and expensive. A small utility-layer should be able to accurately transfer data to batches, log the batch size and not lose connection between inputs and outputs.",
          "blocks": [
            {
              "kind": "code",
              "label": "simple circuit",
              "language": "python",
              "value": "for batch in batched(items, BATCH_SIZE):\n    outputs = call_api(batch)\n    for item, output in zip(batch, outputs):\n        save_result(item, output)"
            }
          ]
        }
      ]
    },
    {
      "number": "07",
      "title": "Practical Playbook And Antipatterns",
      "intro": "",
      "cards": [
        {
          "number": "34",
          "title": "Default mini-stack for most LLM projects",
          "tag": "Ops",
          "description": "Most application pipelines do not need zoo from 20 libraries. Quite often, one reasonable set is enough: `Jinja` for complex templates, `Pydantic` for contracts, `json`, `re`, `pathlib`, `textwrap`, `collections`, `itertools` and `asyncio` as needed.",
          "blocks": [
            {
              "kind": "list",
              "label": "minimum set",
              "items": [
                "Jinja",
                "Pydantic",
                "json / re / pathlib / textwrap",
                "collections / itertools / functools",
                "add the rest for pain, not for fashion"
              ]
            }
          ]
        },
        {
          "number": "35",
          "title": "Frequent anti-patterns",
          "tag": "Ops",
          "description": "Many problems are not in the absence of a library, but in its incorrect use. Unseparated prompt lines, silently missing variables in the template, manual parsing of JSON via `split(\"{\")`, regex instead of normal validation and giant utils.py - all this is then costly.",
          "blocks": [
            {
              "kind": "list",
              "label": "red flags",
              "items": [
                "prompt via chaotic string concatenation",
                "without `StrictUndefined` for complex templates",
                "without `json.loads`/schema validation on model output",
                "regex as the main parser for complex structures"
              ]
            }
          ]
        },
        {
          "number": "36",
          "title": "Quick tool selection",
          "tag": "Bridge",
          "description": "If in doubt, it is useful to run the problem through a short decision tree. Usually this is enough to choose a tool without unnecessary complication.",
          "blocks": [
            {
              "kind": "code",
              "label": "Diagram",
              "language": "text",
              "value": "MINI TREE\n\nDo you just need to insert 2-3 variables?\n  → f-string\n\nNeed loops/if/templates on disk?\n  → Jinja\n\nNeed to accept or return external data?\n  → Pydantic\n\nNeed to quickly clean up a line?\n  → str methods/textwrap/re\n\nNeed to batch/cache/group?\n  → itertools/functools/collections"
            }
          ]
        },
        {
          "number": "37",
          "title": "The main idea of the volume",
          "tag": "Bridge",
          "description": "Around LLM pipelines, it is not the most “AI magic” code that wins, but the most predictable service layer. A good utility stack makes prompts explicit, structures validated, strings clean, data serializable, and runtime observable. And this often provides more benefits than another model change.",
          "blocks": [
            {
              "kind": "list",
              "label": "short reminder",
              "items": [
                "rendering must be explicit",
                "structures need to be validated",
                "lines need to be cleaned boringly and systematically",
                "the simpler the utility layer, the more stable the entire pipeline"
              ]
            }
          ]
        }
      ]
    }
  ]
}
