cd /news/developer-tools/a-successful-python-import-can-still… · home topics developer-tools article
[ARTICLE · art-126314] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

A successful Python import can still lose rows: return a rejection audit

A developer published a Python example showing how an import job can complete without raising an exception while silently discarding rows, and proposed a return-based rejection audit that reports accepted records, rejected row indexes, and counts reconciling with the input. The example uses only the Python 3.12 standard library and deliberately rejects ambiguous conversions such as booleans and fractional numbers rather than coercing them into order quantities. The author notes the code describes a local synthetic-data experiment, not a production incident.

by read4 min views2 publishedSep 10, 2026

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.

── more in #developer-tools 4 stories · sorted by recency
── more on @python 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/a-successful-python-…] indexed:0 read:4min 2026-09-10 ·