In the first function below, the dry run returns "would succeed" before it ever calls target.is_ready(). The apply path calls it. The two modes are answering different questions, so when the real run fails, the environment didn’t change. The preview just never asked.
A dry run should answer one thing: what would happen if we ran this change now? It stops being able to when it takes a shortcut around a precondition the real operation checks. A reassuring preview can be worse than no preview. With none, people know they are operating on uncertainty. With a misleading one, they proceed on confidence they haven’t earned.
The branch that skips the check
The pattern is easy to create. A function branches on a dry_run flag near the top:
def prepare_change(target, dry_run): if dry_run: return "would succeed" if not target.is_ready(): return "cannot proceed" return perform_change(target)Both paths claim to answer “can this change succeed?”, but they use different predicates. The preview is a second program with a friendlier message, and it simulates nothing.
The safer shape evaluates every precondition before the only difference between the modes:
def prepare_change(target, dry_run): if not target.is_ready(): return "cannot proceed" if dry_run: return "would succeed" return perform_change(target)The examples are deliberately simple. In a real system, readiness can include authorization, input validation, ownership, availability, concurrency or lock state, policy checks, dependencies, capacity, or the ability to recover. The dry run should evaluate the same relevant conditions the real change will. Only the side effect gets omitted.
A mismatch means stop
A preview and an apply run won’t always produce identical results. The environment can change between them, an external service can be down, a concurrent user can alter the state. Those are real limits, and a tool should disclose them.
A systematic mismatch is different. It is evidence, and it should halt the operation until someone can explain it. Don’t pick the explanation that lets the change proceed. Look at the difference between what was predicted and what was observed.
- Run the preview against a controlled, authorized scope.
- Record the expected actions, expected skips, and the reason for every decision.
- Validate the same logic with representative, non-sensitive test data.
- Require a responsible person to review the plan before a consequential operation.
- Apply only after that review, using the same scope and guardrails.
- Verify the actual outcome, keep an audit record, and stop if the result diverges materially from the plan.
For destructive or high-impact operations, add safeguards in proportion: least-privilege access, explicit authorization, idempotency, transaction boundaries where they fit, backups or recovery points, rate and resource limits, monitoring, alerting, rollback criteria, and a clear incident path.
Make the preview explain itself
A preview that says “161 items would change” is less useful than one that can say which items would change, which would be skipped, and why. Counts are summaries. The decision record is the evidence.
That record also lets you tell a legitimate state change from a logic defect. If the preview marks an item eligible and the real operation rejects it for a precondition the preview never evaluated, the report shows the gap immediately.
The same goes for automated and AI-assisted workflows. A suggestion, a plan, or a simulation is evidence for a person to review. It doesn’t grant permission to bypass the ordinary controls around data, identity, authorization, and irreversible change.
What a trustworthy dry run is
A dry run earns trust when it is a faithful apply run minus the side effect. Share the decision logic between the two modes and delay the dry_run branch until the final write, call, delete, or update. Make preconditions and expected skips visible. Then treat any meaningful difference between prediction and outcome as a reason to pause, investigate, and fix the tool before trying again.
Related
- Let AI suggest before you save: preserving a human decision boundary before durable data changes
- The runtime was half dead: migration as audit: why artifacts and observed effects are better evidence than a successful-looking status message