cr8 vs python
Python is great. It runs on every machine, it has every library, its standard library is bigger than most languages' entire universe. It is also, by deliberate design, permissive in places where an LLM-written script can quietly produce the wrong answer. Below are four such places. Each is a Python program that runs to completion and does the wrong thing, paired with the cr8script that fails on the bad line and tells you what to fix.
the empty list passes the wrong branch
You filter a list to "high-priority items," then check whether to skip the empty case.
def handle(items): high = [x for x in items if x["priority"] == "high"] if high: # empty list is "false" send_alert(high) else: log("nothing urgent") # silent skip on []
runs to completion. if every item came back filtered out, you log "nothing urgent" and never alert -- even if the upstream filter had a typo and dropped everything.
to handle(items) let high = items | where priority is "high" if high then # error: needs a real boolean send_alert(high) else log("nothing urgent") end end
error at line 3. "if condition must be true or false, got list". Hint: try high.length is greater than 0. The bug shows up at the line, not in production.
string + number silently concatenates
A unit price arrives from a JSON API as "5" (string). You add a service fee.
price = data["price"] # "5" fee = 3 total = price + fee # TypeError # or, in JS-flavoured pseudocode: # total = "5" + 3 -> "53"
python: explicit error — this one Python actually catches. The point is the next round.
let price = data.price # "5" let fee = 3 let total = price + fee # error: can't add text and number # fix: let total = to_number(price) + fee # 8
error at line 3. "can't add text and number -- + won't silently mix types". Hint: convert with to_number or to_text.
"5" == 5 — cross-type equality
Same JSON price arrives as a string. You filter rows where price equals 5.
matches = [r for r in rows if r["price"] == 5] # "5" == 5 -> False (Python is sane here) # [] -- but you expected matches
runs to completion, returns []. The script "works"; the answer is wrong; the writer doesn't know why. Three tools and an hour later: oh, the API returns the price as a string.
let matches = rows | where price is 5 # error: can't compare text ("5") with number (5) # fix: let matches = rows | where to_number(price) is 5
error at the row that mismatches. Refuses to silently return false. The writer is forced to declare what they meant.
== returns false for cross-type comparisons. That looks safe but it's not — an empty result and a false comparison are indistinguishable from "no matches". cr8 makes the conversion explicit.the typo'd field returns None silently
You read customer from each order to print a label. You typo it as cusomer.
for o in orders: print(o.get("cusomer")) # prints None for every row # or with raw access: # o["cusomer"] -> KeyError, but only at the FIRST row # by which time the script may have already done partial work
silent None spam. With .get(), every row "succeeds" and prints None. With [], KeyError fires after side effects start, but the message just names the key — no "did you mean" hint.
for each o in orders show o.cusomer # static check fails BEFORE running end
caught by --check-json before the script runs. "record has no field cusomer. did you mean customer? available: amount, customer, id". The static checker walks the AST and flags it for free.
The pattern in all four: Python is permissive enough for the bug to compile and run; the wrong answer arrives looking plausible. cr8script's refusals make each of these failures located — line, value, hint — and the self-correction loop fixes them in one round trip. That's the trade. Python wins almost everywhere on library breadth and raw speed; cr8 wins on per-script reliability for the kinds of tasks an LLM writes that a human reads and trusts directly.
See it in action: try any of these in the playground. Or read the design defended in principles.