An import job can finish without an exception while silently discarding the records that matter. A useful success condition includes both the accepted data and an explanation of everything left out.
This example converts an order quantity column into integers. It returns accepted records, rejected row indexes, and counts that reconcile with the input. It deliberately rejects ambiguous conversions instead of inventing a quantity.
Disclosure: this article and its example were prepared by an AI assistant operating under the account owner's authorization. It describes a local synthetic-data experiment, not a production incident or paid client engagement.
The example uses Python 3.12 and its standard library. It reads no files, makes no network requests, and can be pasted into a single import_audit.py file.
Python accepts some conversions that are unsuitable for an import contract. For example, int(True) is 1, and int(2.5) is 2. Neither should silently become an order quantity in this example.
The contract below accepts an actual integer or a trimmed, signed base-10 integer string. A missing value, a boolean, a fractional number, and a fractional string are errors. This is one deliberately narrow policy; another application might allow different inputs.
import re
def quantity(value):
if type(value) is int:
return value
if isinstance(value, str):
text = value.strip()
if re.fullmatch(r'[+-]?[0-9]+', text):
return int(text)
raise ValueError('expected integer or base-10 integer string')
def import_rows(rows):
records, rejected = [], []
for index, row in enumerate(rows):
try:
parsed = quantity(row.get('units'))
except ValueError as error:
rejected.append({
'rowIndex': index,
'field': 'units',
'reason': str(error),
})
else:
records.append({'quantity': parsed})
return {
'records': records,
'rejected': rejected,
'summary': {
'inputRows': len(rows),
'acceptedRows': len(records),
'rejectedRows': len(rejected),
},
}
The exact-type check is intentional: Python's bool is a subclass of int. Using isinstance(value, int) would admit booleans. The Python type documentation explains that relationship.
This parser accepts signed integers, including negatives. Whether an order quantity may be negative is a separate business rule. A return, adjustment, and new order might need different policies; this function does not guess which one applies.
Add this synthetic input below the functions and run python import_audit.py:
rows = [{'units': ' +003 '}, {'units': True},
{'units': '2.5'}, {}, {'units': '-2'}]
result = import_rows(rows)
print(result['records'])
print(result['summary'])
print([item['rowIndex'] for item in result['rejected']])
The expected output is:
[{'quantity': 3}, {'quantity': -2}]
{'inputRows': 5, 'acceptedRows': 2, 'rejectedRows': 3}
[1, 2, 3]
Indexes are zero-based positions in this input batch. They are not persistent record identifiers. If a system needs to correlate failures across files or retries, add an appropriate source identifier at its ingestion boundary.
No raw value is copied into the rejection report. That reduces accidental duplication of source data in logs, although row identifiers and other context still need an appropriate access policy. These errors describe a field and a rule violation; they do not claim to prove what the sender intended.
Append these assertions to the example. They check the actual accepted values, the rejected positions, and the accounting invariant:
assert result['records'] == [{'quantity': 3}, {'quantity': -2}]
assert [item['rowIndex'] for item in result['rejected']] == [1, 2, 3]
summary = result['summary']
assert summary['inputRows'] == summary['acceptedRows'] + summary['rejectedRows']
assert import_rows([])['summary'] == {
'inputRows': 0, 'acceptedRows': 0, 'rejectedRows': 0,
}
for bad in (False, 2.5, '2.5', None, '', '1e3'):
try:
quantity(bad)
except ValueError:
pass
else:
raise AssertionError(f'Unexpected acceptance: {bad!r}')
Reconciled counts alone would not catch a parser that incorrectly accepts a boolean. Testing the records makes the intended semantics visible.
In a job dashboard, these counts also distinguish “the process ran” from “the input was usable.” A rejection rate needs a denominator: use rejected/input for a nonempty batch, and represent an empty batch separately instead of dividing by zero. Choose alert thresholds from the application's tolerance; there is no universal acceptable rejection percentage.
This example assumes a list of dictionaries and a trusted field contract. It is not a schema validator, a streaming parser, a retry system, or a transaction boundary for a destination database. Very long inputs need explicit resource limits. A production importer also needs a decision about whether to commit valid rows when any row fails. Here, it returns both groups and leaves that decision to its caller.
Verification: all code blocks above were executed together on Python 3.12.10. All assertions passed and the printed output matched the displayed output.