Skip to content

Magic-string typing — bare str where a brand or enum would catch errors

The corpus prior

Python's stdlib str is the universal carrier. Tutorials use it for IDs (user_id: str), for status fields (status: str with conditional dispatch on literals like "pending" / "approved"), and for typed lookup keys (record["status"]). The corpus is dominated by this shape because str is the path of least resistance and the type checker doesn't complain.

Mix-ups (fetch(tenant_id, user_id) instead of fetch(user_id, tenant_id)) typecheck cleanly and surface as data corruption later — the kind of bug that takes a long time to find because the symptom is "the wrong customer got the wrong invoice" rather than a stack trace.

Typos in dispatch literals (elif status == "aprroved":) never fire at runtime and silently fall through. The cost compounds because the corpus's bias makes the pattern feel natural even to agents that should know better.

Wrong shape

Three sub-shapes covered by the catalogue. PA-LLM-10 currently detects sub-shape (a) only; (b) and (c) are documented for inference-time guidance.

(a) Magic-string IDs:

def transfer_funds(source_id: str, destination_id: str, amount: int) -> Result[Receipt, TransferError]:
    ...

# Caller:
transfer_funds(destination_id, source_id, amount)  # arguments swapped, type-checker happy

(b) Enum-dispatch chains:

def render_status_badge(status: str) -> str:
    if status == "pending":
        return "⏳"
    elif status == "approved":
        return "✅"
    elif status == "rejected":
        return "❌"
    # typo: "aprroved" silently falls through
    return ""

(c) Typed lookup keys (informational — not detected by PA-LLM-10):

def get_field(record: dict, key: str) -> object:
    return record[key]  # any string is acceptable; no type-checker leverage

Right shape

Three patterns matching the three sub-shapes:

(a) Branded IDs via dazzle.types.NewType:

# app/ids.py
from dazzle.types import NewType

UserId = NewType("UserId", str)
TenantId = NewType("TenantId", str)
PaymentId = NewType("PaymentId", str)
# app/transfers.py
from app.ids import PaymentId

def transfer_funds(source_id: PaymentId, destination_id: PaymentId, amount: int) -> Result[Receipt, TransferError]:
    ...

# Caller:
src = PaymentId(row["src_id"])
dst = PaymentId(row["dst_id"])
transfer_funds(src, dst, amount)  # both are PaymentId — no swap risk between distinct ID classes

The brand catches mix-ups between different ID classes (UserId vs TenantId). Within a single ID class, parameter-ordering bugs are still possible — the type system catches the harder cross-class error.

(b) Closed sets via enum.StrEnum:

from enum import StrEnum


class OrderStatus(StrEnum):
    PENDING = "pending"
    APPROVED = "approved"
    REJECTED = "rejected"


def render_status_badge(status: OrderStatus) -> str:
    match status:
        case OrderStatus.PENDING:
            return "⏳"
        case OrderStatus.APPROVED:
            return "✅"
        case OrderStatus.REJECTED:
            return "❌"
    # type checker (with exhaustive match) catches missing cases

Typos in the StrEnum class definition fail loudly at class-definition time; typos in match arms typecheck as errors. The str value is preserved (OrderStatus.PENDING == "pending" is True) so JSON serialization works without changes.

(c) TypedDict for record keys (informational):

from typing import TypedDict


class OrderRecord(TypedDict):
    status: OrderStatus
    amount: int
    customer_id: UserId


def get_status(record: OrderRecord) -> OrderStatus:
    return record["status"]  # mypy catches typos in the key

Convention notes:

  • Declare branded types in app/ids.py (or similar) — co-located with their domain, importable from anywhere in app/.
  • Use enum.StrEnum (stdlib, Python 3.11+) for closed value sets. Don't reach for a library; the stdlib is enough.
  • NewType is runtime-free — UserId("x") returns the plain str "x". The brand is type-checker-only. This is the right tradeoff: zero runtime cost, full static safety.

Why this matters here

Dazzle's framework code paths use typed IR (EntityRef, PersonaSpec, etc.) and DSL-level enums (enum keyword) that prevent magic-string typing at the model layer. User-app Python in app/ doesn't yet have an idiomatic answer — agents reach for str because the corpus does.

PA-LLM-10 flags ID-shaped parameters at scan time. dazzle.types makes the right shape one import away. The catalogue documents all three sub-shapes for inference-time agent guidance even though only (a) is detected today.

StrEnum is stdlib and not part of dazzle.types for the same reason dataclasses isn't — it's already in the standard library and a re-export would add nothing.