The same check written two ways. The difference is not the logic, it is how much the caller learns per attempt.
# Fails at the first problem and hides the rest
for name in required:
if name not in record:
raise ValueError(f'missing {name}')
# Gathers everything, then reports once
missing = [name for name in required if name not in record]
if missing:
raise ValueError('missing: ' + ', '.join(missing))
- Raising inside the loop throws away what you have not checked
The moment you raise, the remaining fields are never examined — so a record with four problems takes four submissions to fix. Building a list first costs one extra line and turns that into one.
- The empty list is the condition
`if missing:` is true only when something went wrong, so the collecting version has exactly one decision point instead of one per field. That shape — gather, then act once — is worth recognising well beyond validation.
- Order comes from the requirements, not the record
Iterating over `required` rather than over the record means the message lists fields in the order the caller declared them, which is the order they appear on the form. Iterating the record would produce a different order for every input.
Work out the full list of problems before deciding what to do about them. And be careful about what 'has a value' means — one of the cases is about a field holding zero.
Build a list of the names that fail, using a comprehension over fields. A name fails when it is not in the record, or its value is None, or its value is an empty string — three specific conditions, not `if not value`.
missing = [name for name in fields if name not in record or record[name] is None or record[name] == '']. Then if missing, raise ValueError with 'missing required field(s): ' followed by the names joined with ', '. Otherwise return record.
def require_fields(record, fields): missing = [ name for name in ____ if name not in record or record[name] is ____ or record[name] == '____' ] if ____: raise ValueError('missing required field(s): ' + ', '.____(missing)) return ____
def require_fields(record, fields):
missing = [
name
for name in fields
if name not in record or record[name] is None or record[name] == ""
]
if missing:
raise ValueError("missing required field(s): " + ", ".join(missing))
return record
`if not record.get(name)` is shorter and treats 0, False and [] as missing — three cases here exist to catch exactly that. Absence and emptiness are not the same as falsity, and a validator that confuses them rejects perfectly good data. Being specific costs a few characters and buys correctness.
`user = require_fields(payload, ['name', 'email'])` reads as one step, where a version returning None forces a separate call before the assignment. Returning the thing you validated is a small courtesy that makes a validator composable — and it costs one line.
Two of the cases check the message itself, because the message *is* the feature. A validator that reports one problem at a time is technically correct and practically hostile. When you catch yourself raising inside a loop, ask whether the caller would rather hear all of it at once.
For anything with real structure — nested objects, types, formats, optional fields with defaults — use a schema library. Pydantic and marshmallow already collect every error with a path to each one, and they validate types at the same time, which this does not. Write this when you have a flat dictionary and one rule. And note the definition of 'missing' here is a policy: some systems treat an empty string as a deliberate blanking, distinct from absence, and then this function is wrong for them.
- Return the list of missing fields instead of raising, and write require_fields on top of it — then decide which one belongs in the public interface.
- Add per-field rules, so email must also contain an @, and see how quickly you start wanting a schema library.
- Support nested paths like 'address.postcode' by reusing the deep-get exercise, and work out what the error message should say when the parent is missing entirely.