Parsing turns ACH record lines into accessible fields that application code can use. A parser separates each record type into its own routine, then reads fixed character ranges into named dictionary values such as priority code, institution identifiers, account number, and amount. This keeps record-specific layout rules visible rather than burying them in file-loading logic. section 2.4.2
Listing 2.5 Starting point for our ACH parser
def parse_file_header(line):
return {
'record_type': 'File Header',
'priority_code': line[1:3],
'immediate_destination': line[3:13],
'immediate_origin': line[13:23],
# ... additional fields
}
def parse_batch_header(line):
return {
'record_type': 'Batch Header',
'service_class_code': line[1:4],
'company_name': line[4:20],
# ... additional fields
}
def parse_entry_detail(line):
return {
'record_type': 'Entry Detail',
'transaction_code': line[1:3],
'receiving_dfi_id': line[3:11],
'account_number': line[12:29],
'amount': line[29:39],
# ... additional fields
}
def parse_addenda(line):
return {
'record_type': 'Addenda',
# ... additional fields based on the specific addenda type
}
def parse_batch_control(line):
return {
'record_type': 'Batch Control',
# ... additional fields
}
def parse_file_control(line):
return {
'record_type': 'File Control',
# ... additional fields
}
def parse_ach(filename):
records = []
parser_functions = {
'1': parse_file_header,
'5': parse_batch_header,
'6': parse_entry_detail,
'7': parse_addenda,
'8': parse_batch_control,
'9': parse_file_control
}
with open(filename, 'r') as file:
lines = file.readlines()
for line in lines:
record_type = line[0]
parser = parser_functions.get(record_type)
if parser:
records.append(parser(line))
else:
print(f"Unknown record type: {record_type} in line: {line}")
continue
return records
# Test the function with an ACH file
parsed_records = parse_ach('path_to_ach_file.txt')
for record in parsed_records:
print(record)
The starter routines show the core pattern: identify a record type, slice its fixed-width positions, and return a dictionary of named fields. Separate functions make file header, batch header, entry detail, and addenda layouts easier to extend and test independently. section 2.4.2
Start with a narrow parsing goal before adding broader processing behavior. Parsing alone does not guarantee safe input: defensive coding also calls for input validation, error handling, fail-safe defaults, output sanitization, logging or monitoring, and static analysis. Treat unexpected lengths, invalid record types, and unusual whitespace as inputs that need explicit decisions rather than assumptions. section 2.4.2 section 2.4.3
Verify a file header parser
Pass a sample type-1 file-header line to a parser and compare its returned dictionary with an expected result. The check can reveal layout mistakes, such as interpreting an eight-position creation date where the format expects six or accepting a record that is not 94 bytes long. A failing comparison makes the differing fields visible; tests can also assert that malformed records raise errors. section 2.4.3
Use small development cycles to make parsing changes safer:
- Break record handling into functions so each record type can be checked independently. section 2.5.4
- Write a unit test that fails for the required behavior, then implement only enough parsing logic to make it pass. section 2.5.4
- Check generated code against sample records and expected output; generated assumptions about lengths and field positions can be wrong. section 2.4.3
- Preserve existing batch behavior while integrating new record types, including the presence and order of addenda records. section 11.5
Parsing individual records is only one part of file processing. Once individual routines are verified, integrate them into the actual ACH-file loading flow and confirm that the resulting records are stored as expected. For IAT batches, that means ensuring the IAT batch-header routine is actually called while ordinary PPD batches continue to work and addenda remain complete and ordered. section 11.5












