hamsa software
← All articles
Engineering7 min read

Read the change before its explanation

A reassuring summary cannot tell you whether a validator preserves old identifiers, rejects trailing text or understands the contract. Eight small inputs can.

A circular geometric lens overlaps a plane with eight lines, suggesting close examination of code against independent contract cases.

A very plausible summary

The summary promises strict validation without breaking compatibility. Two tests pass. Reading the expression, though, shows that an old customer identifier has no way through it, while a valid-looking new one can bring extra text along. The example is authored for this review exercise, so we can inspect both disagreements directly. It is the gap between the very plausible explanation and the function’s actual answer that deserves the reviewer’s attention.

An assistant’s summary can be a useful introduction to a patch, just as a colleague’s can. In either case, it describes what the author thinks the change does. I would first read the requirement and the diff here, then return to the account of them. An old identifier gives that reading a concrete question to resolve before the summary supplies its own explanation of success.

The initial question is small enough to answer from examples: which complete customer identifier strings does this interface promise to accept? Keep the two accepted formats in view while reading, rather than trying to judge whether the regular expression looks sufficiently strict. Once those examples have an independent place in the review, the later explanation can help account for the code instead of supplying the only definition of success.

Put the compatibility promise into examples

The example contract accepts two complete formats: a legacy identifier such as AB-1234, and a current identifier such as CUST-2025-0007. Prefix letters are uppercase ASCII and the digit positions contain ASCII digits. Leading or trailing material is invalid. Empty input is invalid. These choices belong to this invented interface; another system might correctly accept a wider character set or normalise whitespace before validation.

That last distinction matters. Normalising an identifier and validating an identifier are different operations. If surrounding whitespace is permitted at an input boundary, decide where it is removed and whether the stored identifier changes. Quietly trimming it inside one validator can make two services disagree about the same submitted string. Equally, a blanket ban on Unicode would be an absurd general rule for customer names. Here the narrow alphabet belongs to a specific machine identifier contract.

Before reading the proposed tests, set out examples for each obligation: an old identifier, a new identifier, extra text after a valid-looking prefix, a different digit family, an empty string, wrong letter case, a leading character and a trailing newline. Each probes a separate interpretation of the contract. The cases are useful because their expected outcomes come from that agreement, independently of which examples the implementation already handles.

Follow the expression all the way to its return value

The proposed function first checks that the value is a string, then calls re.match with the expression ^CUST-\d{4}-\d{4}. It returns true whenever a match exists. The current example passes, and empty input fails. Those two outcomes can make an accompanying test file appear reassuring without touching the compatibility promise at all. AB-1234 never has a route through the expression: its rejection follows directly from the required CUST prefix.

The next problem is where matching stops. Python documents re.match as matching at the beginning of the string; it does not require the pattern to consume the whole string. [1] A valid-looking identifier followed by extra text therefore succeeds. So does the same prefix followed by a newline. The code has checked for a recognisable beginning while its explanation promises a valid complete value. That is a behavioural difference, rather than a disagreement about preferred regular-expression style.

There is another small but consequential choice in \d. For Unicode string patterns, Python’s documentation describes it as matching Unicode decimal digits, not just the characters zero through nine. [1] The Arabic-Indic example in the fixture therefore passes the proposed expression. A reviewer cannot settle whether that is a bug from the expression alone; the interface contract settles it here. Replacing \d with [0-9] is appropriate because this particular contract requires ASCII digits, not because wider character support is inherently suspect.

Finally, appending a dollar sign is not a complete repair. Python documents its behaviour before a final newline, and it would still leave the legacy-format omission and the digit-class decision unresolved. The corrected teaching function uses re.fullmatch with an explicit alternative for both formats. Full-string matching answers the boundary question; the alternatives answer compatibility; the character classes answer the alphabet. Three promises require three separate decisions.

The contract asks eight different questions

An original proposed validator passes its two narrow self-tests but matches only four of the eight independently specified outcomes. The corrected expression is checked against the same inputs.

Scroll sideways to see all columns.

Input caseContractProposedCorrected
AB-1234 · legacyAcceptRejectAccept
CUST-2025-0007 · currentAcceptAcceptAccept
Current ID + extraRejectAcceptReject
Arabic-Indic digitsRejectAcceptReject
Empty stringRejectRejectReject
Lowercase prefixRejectRejectReject
Leading textRejectRejectReject
Trailing newlineRejectAcceptReject
Authored toy code and synthetic identifiers, not a captured assistant output or model benchmark. The downloadable Python file preserves the exact strings, including the newline and Unicode digits.

Tests need an independent job

The downloadable program contains both authored implementations. Its narrow self-test set checks only the current identifier and empty input: the proposed implementation passes both. The independent eight-case set gives it four correct outcomes, while the corrected expression matches all eight expected outcomes. Keep the failing strings available alongside those counts. A four-of-eight result names a disagreement, but the legacy value, trailing material and digit family tell the reviewer which promises the implementation omitted.

Four failed cases need not mean four independent repairs. Trailing text and a final newline both expose prefix-only matching; rejecting the legacy format and accepting another digit family concern different promises. Group the failures by the rule each exercises before changing the code. That gives the reviewer a reason for each part of the corrected expression, rather than eight isolated answers to memorise. These are contract examples, not a sample from which to estimate how often AI-written code fails.

The downstream use helps decide how much the mistakes matter. If an import still contains AB-1234 records, rejecting them can interrupt a migration that was supposed to remain compatible. If search accepts trailing text but storage rejects it, the same submitted identifier appears valid at one step and fails at the next. Those are better reasons to change the expression than a preference for shorter or cleverer regex. They also give the author a way to understand why a small-looking validation comment matters to somebody trying to complete ordinary work. The review can stay focused on compatibility and complete-input behaviour rather than become a contest over pattern style.

These eight inputs are strings. If callers can also supply numbers or null, decide whether to reject them or perform an explicit conversion before pattern matching. Quietly converting an account number to text can change which inputs the interface accepts, just as silently trimming whitespace can. Put those outcomes beside the existing boundary cases so the review preserves the intended input contract.

Three regex decisions protect three contract promises

The identifier exercise keeps compatibility, input boundary and alphabet decisions independent.

Scroll sideways to see all columns.

PromiseInsufficient repairRequired decision in the fixture
Legacy AB-1234 remains acceptedAdd an end anchor to a CUST-only expressionInclude both accepted formats
Trailing text is rejectedre.match checks only a beginningUse complete-string matching
Digit positions are ASCIIUnicode \d admits other decimal digitsUse the contract’s explicit character class
Final newline is rejectedA dollar anchor has documented newline behaviourCheck complete input with fullmatch
These rules belong to the authored identifier interface; they are not a general argument against Unicode or normalisation.

A review comment can carry the contract forward

A useful review comment would name the observed behaviour: “AB-1234 is part of the legacy contract, but this expression rejects it; CUST-2025-0007extra is outside the contract, but it passes.” It can then ask for full-string matching, explicit format alternatives and tests for the exact boundary cases. This is more actionable than “make validation more robust”, which leaves the author to guess which promise the reviewer thinks is missing.

Keep the eventual repair proportionate. This change does not require an identifier framework, a new dependency or a wholesale rewrite of the surrounding customer flow. It does require agreement on how existing records enter the system and where normalisation occurs. If the real interface specification is unclear, record that uncertainty rather than allowing whichever regex arrived first to become the accidental business rule.

After revision, follow the expected answers as well as the expression. GitHub’s review guidance makes the changed lines and their discussion available together. [2] AB-1234 should still expect acceptance; CUST-2025-0007extra should still expect rejection. If the patch instead changes those expectations to match the faulty function, its green run records a withdrawn compatibility promise and an expanded input boundary. Compare each new expectation with the original interface before approving it.

Now return to the summary. It should say that both formats remain accepted, that matching covers the complete input and that the digit alphabet is explicit. The next reviewer can follow each claim into one of the eight expected outcomes without reconstructing the prompting conversation. That is a useful job for an explanation: preserve the reasoning behind the change after the behaviour has been checked, so the next tidy-up does not accidentally reopen the same agreement.

Working on a workflow like this? Hamsa helps finance and operations teams connect their tools and handle the exceptions, starting with one workflow.

See the Payment Operations Pilot

References & further reading

Primary sources behind this article. Living documentation may change.

  1. Python: regular expressions, match, fullmatch and character classes
  2. GitHub: reviewing changes in pull requests
Hamsa Jama

Practical engineering. A human perspective.

More about Hamsa Software and the person behind it. Editorial notes.

A few more things
worth a read.

Back to the reading room
Engineering7 min read

What does £240 belong to?

A readable table preserves relationships between values, row labels, column headings and actions across different ways of reading.

Engineering7 min read

When two calculations agree on the wrong answer

A shadow comparison exposes differences. Independent rule examples explain whether those differences are bugs, corrections or changed assumptions.