{"course":{"slug":"how-software-changed","title":"How Software Development Changed","url":"https://addaly.com/learn/how-software-changed","level":"intermediate","track":"work"},"lesson":{"slug":"safety-that-went-missing","title":"Safety that quietly went missing","url":"https://addaly.com/learn/how-software-changed/safety-that-went-missing","position":16,"of":87,"minutes":9,"answer":"Removed checks are the highest-risk lines in a diff and the ones human attention least reliably reaches.","keyIdea":"Removed checks are the highest-risk lines in a diff and the ones human attention least reliably reaches, so the fix is a mechanical grep over deletions plus a norm that every removed guard is explained in the description."},"body":{"format":"markdown","text":"## The highest-yield lines in any diff are the deletions\n\nWhen a change removes a check, the diff shows a red line and moves on. There is no compiler error, no failing test (the test that would have failed is often the one that was deleted) and no visual signal that anything important happened. In a 700-line review these lines are three of them, and they carry most of the risk.\n\nThis is not new. What is new is the mechanism that produces them. Asked to make something work, a generator will make it work, and the shortest path from failing to passing frequently runs straight through a safety check. Nothing in that process is malicious or even careless. The check was in the way and the objective was to remove the obstacle.\n\n## The catalogue\n\nLearn these shapes so you see them without looking for them.\n\n**The widened net.**\n\n```python\n-except ValidationError as e:\n-    logger.warning(\"bad payload: %s\", e)\n-    return 400\n+except Exception:\n+    pass\n```\n\nEvery bug in that block is now invisible. `except Exception: pass` and its equivalents (`catch (e) {}`, `rescue nil`, `if err != nil { }`) are the single most common way a system loses the ability to tell you what is wrong.\n\n**The loosened type.**\n\n```typescript\n-function total(items: LineItem[]): Money\n+function total(items: any[]): any\n```\n\nOr in Python, `# type: ignore`; in Rust, `unsafe` or a fresh `.unwrap()`; in Java, a raw type or an `@SuppressWarnings`. Each of these switches off an oracle that was running on every build, for ever, for free.\n\n**The disabled rule.** `// eslint-disable-next-line`, `# noqa`, `#pragma warning disable`, `@ts-expect-error`. Sometimes correct. Always deserving of a comment saying why, on the same line.\n\n**The skipped or weakened test.**\n\n```python\n-@pytest.mark.parametrize(\"currency\", CURRENCIES)\n+@pytest.mark.skip(reason=\"flaky\")\n```\n\n\"Flaky\" is doing a great deal of work in that reason string. A flaky test is a defect report about the system or the test, and skipping it is a decision to stop receiving the report.\n\n**The relaxed constraint.** A `NOT NULL` dropped, a `UNIQUE` index removed, a foreign key made deferrable, a `CHECK` deleted in a migration. These are the most expensive of all, because a constraint removal is followed within weeks by data that violates it, and then you cannot put it back.\n\n**The raised limit.** A timeout from 5s to 60s. A retry count from 3 to 10. A connection pool doubled. A memory limit raised. Each of these is a real fix in some situations and a way of hiding a problem in most, and none of them should pass review without a sentence explaining which.\n\n**The widened permission.** A scope added, a role check removed, a bucket policy broadened, a CORS origin set to `*`, a firewall rule opened. Narrowing takes thought; widening always works.\n\n## Make the machine find them\n\nHumans miss deletions because attention follows additions. A pipeline does not have that bias.\n\n```bash\n#!/usr/bin/env bash\n# flag safety subtractions for explicit review\ngit diff origin/main...HEAD -U0 \\\n | grep -E '^\\-' \\\n | grep -viE '^\\-\\-\\-' \\\n | grep -nE 'assert|raise|throw|validate|NOT NULL|UNIQUE|CHECK |require|permission|scope|@pytest|test_|if .*(None|null|nil)' \\\n && echo \"^ removed lines above touch validation or safety — explain each in the PR description\"\n```\n\nThis is deliberately noisy. Its job is not to block; its job is to make a human write a sentence. A rule that produces a short list a person reads is worth more than a precise rule nobody configured.\n\nOn the additions side, a matching grep for `except Exception`, `: any`, `type: ignore`, `eslint-disable`, `unwrap()`, `skip(` and `sleep(` catches the other half.\n\n## The norm that makes it work\n\nOne line in the contributing guide, and it is worth arguing for:\n\n> Any change that removes a validation, widens an exception handler, loosens a type, skips a test, drops a database constraint, or broadens a permission must say why in the pull request description.\n\nNot forbidden. Explained. All of these are sometimes right: the constraint was wrong, the test was measuring the clock, the type is dynamic at that boundary. What is never right is doing it silently, because the silence is what makes it undiscoverable six months later when somebody asks how the null got into the column.\n\nAnd when the explanation is missing, the question to ask is not \"why did you remove this\" but the more useful one: **what was this protecting against, and what protects against it now?**"},"check":{"question":"A change makes a failing integration test pass by widening a `try/except ValidationError` to `except Exception`. All tests are now green. Why is this worse than the original failure?","options":[{"text":"Catching a broad exception type is slower at runtime, and in a hot path that cost compounds.","correct":false,"tempting":"Reaches for a performance argument; the cost of exception matching is negligible next to losing the failure signal."},{"text":"It hides every future failure in that block, not just this one.","correct":true},{"text":"It violates the style guide, and inconsistent error handling makes the codebase harder to maintain over time.","correct":false,"tempting":"Frames a correctness loss as a consistency issue, which invites the fix of applying the same pattern everywhere."},{"text":"The original test was measuring something real, so the correct action was to delete the test and file a ticket describing the underlying defect.","correct":false,"tempting":"Right that the signal was real, wrong about the remedy: deleting the test discards the same signal the widened handler discards."}]},"prev":{"slug":"reading-the-tests","title":"Reading a test suite you did not write","url":"https://addaly.com/learn/how-software-changed/reading-the-tests"},"next":{"slug":"reviewing-dependency-changes","title":"Reviewing a dependency change","url":"https://addaly.com/learn/how-software-changed/reviewing-dependency-changes"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Safety that quietly went missing', in How Software Development Changed. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/how-software-changed/safety-that-went-missing (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Safety that quietly went missing","courseTitle":"How Software Development Changed","module":{"index":2,"slug":"reading-what-you-did-not-write","title":"Reading what you did not write"},"answer":null,"keyIdea":"Removed checks are the highest-risk lines in a diff and the ones human attention least reliably reaches, so the fix is a mechanical grep over deletions plus a norm that every removed guard is explained in the description.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"the-highest-yield-lines-in-any-diff-are-the-deletions","runs":[{"text":"The highest-yield lines in any diff are the deletions"}]},{"t":"p","runs":[{"text":"When a change removes a check, the diff shows a red line and moves on. There is no compiler error, no failing test (the test that would have failed is often the one that was deleted) and no visual signal that anything important happened. In a 700-line review these lines are three of them, and they carry most of the risk."}]},{"t":"p","runs":[{"text":"This is not new. What is new is the mechanism that produces them. Asked to make something work, a generator will make it work, and the shortest path from failing to passing frequently runs straight through a safety check. Nothing in that process is malicious or even careless. The check was in the way and the objective was to remove the obstacle."}]},{"t":"h2","id":"the-catalogue","runs":[{"text":"The catalogue"}]},{"t":"p","runs":[{"text":"Learn these shapes so you see them without looking for them."}]},{"t":"p","runs":[{"text":"The widened net.","b":true}]},{"t":"code","lang":"python","label":"Python","text":"-except ValidationError as e:\n-    logger.warning(\"bad payload: %s\", e)\n-    return 400\n+except Exception:\n+    pass"},{"t":"p","runs":[{"text":"Every bug in that block is now invisible. "},{"text":"except Exception: pass","code":true},{"text":" and its equivalents ("},{"text":"catch (e) {}","code":true},{"text":", "},{"text":"rescue nil","code":true},{"text":", "},{"text":"if err != nil { }","code":true},{"text":") are the single most common way a system loses the ability to tell you what is wrong."}]},{"t":"p","runs":[{"text":"The loosened type.","b":true}]},{"t":"code","lang":"typescript","label":"TypeScript","text":"-function total(items: LineItem[]): Money\n+function total(items: any[]): any"},{"t":"p","runs":[{"text":"Or in Python, "},{"text":"# type: ignore","code":true},{"text":"; in Rust, "},{"text":"unsafe","code":true},{"text":" or a fresh "},{"text":".unwrap()","code":true},{"text":"; in Java, a raw type or an "},{"text":"@SuppressWarnings","code":true},{"text":". Each of these switches off an oracle that was running on every build, for ever, for free."}]},{"t":"p","runs":[{"text":"The disabled rule.","b":true},{"text":" "},{"text":"// eslint-disable-next-line","code":true},{"text":", "},{"text":"# noqa","code":true},{"text":", "},{"text":"#pragma warning disable","code":true},{"text":", "},{"text":"@ts-expect-error","code":true},{"text":". Sometimes correct. Always deserving of a comment saying why, on the same line."}]},{"t":"p","runs":[{"text":"The skipped or weakened test.","b":true}]},{"t":"code","lang":"python","label":"Python","text":"-@pytest.mark.parametrize(\"currency\", CURRENCIES)\n+@pytest.mark.skip(reason=\"flaky\")"},{"t":"p","runs":[{"text":"“Flaky” is doing a great deal of work in that reason string. A flaky test is a defect report about the system or the test, and skipping it is a decision to stop receiving the report."}]},{"t":"p","runs":[{"text":"The relaxed constraint.","b":true},{"text":" A "},{"text":"NOT NULL","code":true},{"text":" dropped, a "},{"text":"UNIQUE","code":true},{"text":" index removed, a foreign key made deferrable, a "},{"text":"CHECK","code":true},{"text":" deleted in a migration. These are the most expensive of all, because a constraint removal is followed within weeks by data that violates it, and then you cannot put it back."}]},{"t":"p","runs":[{"text":"The raised limit.","b":true},{"text":" A timeout from 5s to 60s. A retry count from 3 to 10. A connection pool doubled. A memory limit raised. Each of these is a real fix in some situations and a way of hiding a problem in most, and none of them should pass review without a sentence explaining which."}]},{"t":"p","runs":[{"text":"The widened permission.","b":true},{"text":" A scope added, a role check removed, a bucket policy broadened, a CORS origin set to "},{"text":"*","code":true},{"text":", a firewall rule opened. Narrowing takes thought; widening always works."}]},{"t":"h2","id":"make-the-machine-find-them","runs":[{"text":"Make the machine find them"}]},{"t":"p","runs":[{"text":"Humans miss deletions because attention follows additions. A pipeline does not have that bias."}]},{"t":"code","lang":"bash","label":"Bash","text":"#!/usr/bin/env bash\n# flag safety subtractions for explicit review\ngit diff origin/main...HEAD -U0 \\\n | grep -E '^\\-' \\\n | grep -viE '^\\-\\-\\-' \\\n | grep -nE 'assert|raise|throw|validate|NOT NULL|UNIQUE|CHECK |require|permission|scope|@pytest|test_|if .*(None|null|nil)' \\\n && echo \"^ removed lines above touch validation or safety — explain each in the PR description\""},{"t":"p","runs":[{"text":"This is deliberately noisy. Its job is not to block; its job is to make a human write a sentence. A rule that produces a short list a person reads is worth more than a precise rule nobody configured."}]},{"t":"p","runs":[{"text":"On the additions side, a matching grep for "},{"text":"except Exception","code":true},{"text":", "},{"text":": any","code":true},{"text":", "},{"text":"type: ignore","code":true},{"text":", "},{"text":"eslint-disable","code":true},{"text":", "},{"text":"unwrap()","code":true},{"text":", "},{"text":"skip(","code":true},{"text":" and "},{"text":"sleep(","code":true},{"text":" catches the other half."}]},{"t":"h2","id":"the-norm-that-makes-it-work","runs":[{"text":"The norm that makes it work"}]},{"t":"p","runs":[{"text":"One line in the contributing guide, and it is worth arguing for:"}]},{"t":"quote","runs":[{"text":"Any change that removes a validation, widens an exception handler, loosens a type, skips a test, drops a database constraint, or broadens a permission must say why in the pull request description."}]},{"t":"p","runs":[{"text":"Not forbidden. Explained. All of these are sometimes right: the constraint was wrong, the test was measuring the clock, the type is dynamic at that boundary. What is never right is doing it silently, because the silence is what makes it undiscoverable six months later when somebody asks how the null got into the column."}]},{"t":"p","runs":[{"text":"And when the explanation is missing, the question to ask is not “why did you remove this” but the more useful one: "},{"text":"what was this protecting against, and what protects against it now?","b":true}]}],"check":{"question":[{"text":"A change makes a failing integration test pass by widening a "},{"text":"try/except ValidationError","code":true},{"text":" to "},{"text":"except Exception","code":true},{"text":". All tests are now green. Why is this worse than the original failure?"}],"options":[{"text":[{"text":"Catching a broad exception type is slower at runtime, and in a hot path that cost compounds."}],"correct":false,"tempting":[{"text":"Reaches for a performance argument; the cost of exception matching is negligible next to losing the failure signal."}]},{"text":[{"text":"It hides every future failure in that block, not just this one."}],"correct":true,"tempting":null},{"text":[{"text":"It violates the style guide, and inconsistent error handling makes the codebase harder to maintain over time."}],"correct":false,"tempting":[{"text":"Frames a correctness loss as a consistency issue, which invites the fix of applying the same pattern everywhere."}]},{"text":[{"text":"The original test was measuring something real, so the correct action was to delete the test and file a ticket describing the underlying defect."}],"correct":false,"tempting":[{"text":"Right that the signal was real, wrong about the remedy: deleting the test discards the same signal the widened handler discards."}]}],"order":[3,0,1,2],"why":null},"prev":{"slug":"reading-the-tests","title":"Reading a test suite you did not write","minutes":10},"next":{"slug":"reviewing-dependency-changes","title":"Reviewing a dependency change","minutes":9}}}