Generating fixed-width test data with Python and reading it in COBOL
A Python script writes a 64-byte fixed-width transaction file, a copybook pins the layout, and a GnuCOBOL program reads it back and prints control totals.
You get three files that agree with each other: a Python script that writes 200 fixed-width transaction records, a copybook that describes one record, and a COBOL program that reads the file and prints control totals. The point of the lesson is the agreement. Fixed-width data has no delimiters and no header row, so the only thing that makes it readable is a layout that both sides honour to the byte. In a COBOL shop that layout lives in a copybook, and the copybook is the contract.
We ran the Python script on this machine, so the data file and the totals quoted below are real. The COBOL listing was desk-checked against the GnuCOBOL 3.2 Programmer's Guide and written to print exactly the totals the Python parser printed; on 29 October 2026 we compiled and ran it with GnuCOBOL 3.2, and it does, with one correction to how the net line looks (see the note at the end). If you compile it and the numbers differ, the copybook is the first place to look. The lesson builds on reading a sequential file and on PICTURE clauses, and it is worth having both fresh.
Record layout
Every line of the data file is exactly 64 bytes followed by a line feed. The copybook in /code/python-fixed-width-test-data-cobol.cpy describes the same 64 bytes for COBOL.
| Field | Columns | PIC | Meaning |
|---|---|---|---|
| Txn-Id | 1 to 8 | 9(8) | Sequence number, zero padded |
| Txn-Account | 9 to 14 | 9(6) | Account number, zero padded |
| Txn-Type | 15 to 16 | X(2) | DR (debit) or CR (credit) |
| Txn-Amount | 17 to 27 | 9(9)V99 | Amount in cents, 11 digits, no decimal point |
| Txn-Date | 28 to 35 | 9(8) | YYYYMMDD |
| Txn-Memo | 36 to 64 | X(29) | Description, left justified, space padded |
The one column that catches people is the amount. PIC 9(9)V99 is eleven digit positions. The V is an implied decimal point: it tells the compiler where the point sits, but it occupies no storage, which the Programmer's Guide states plainly in its PICTURE section. So a value of 1,234.56 is stored as the characters 00000123456, and Python has to write it that way. If the script wrote 1234.56, COBOL would read a period in a digit position and the field would fail its NUMERIC test.
The copybook
*> python-fixed-width-test-data-cobol.cpy
*> 64-byte transaction record shared by the Python generator and the
*> COBOL reader. Comment lines start in column 7 so the copybook is
*> legal in both fixed and free source format.
*> Status: desk-checked against the GnuCOBOL 3.2 Programmer's Guide.
01 Txn-Record.
05 Txn-Id PIC 9(8).
05 Txn-Account PIC 9(6).
05 Txn-Type PIC X(2).
88 Txn-Debit VALUE "DR".
88 Txn-Credit VALUE "CR".
05 Txn-Amount PIC 9(9)V99.
05 Txn-Date PIC 9(8).
05 Txn-Memo PIC X(29).
The copybook is indented so that its level numbers begin in column 8 and its comment lines carry an asterisk in column 7. That makes it usable from a fixed-format program as well as from the free-format program below, which is how copybooks get shared across generations of code. The two level-88 condition names give the reader Txn-Debit and Txn-Credit as plain conditions instead of string comparisons.
The Python generator
The script in /code/python-fixed-width-test-data-cobol.py seeds the random generator with 20260905 so the file is reproducible. The interesting lines are the ones that format each field.
txn_id = f"{n:08d}" # PIC 9(8): zero padded on the left
account = f"{rng.choice(ACCOUNTS):06d}" # PIC 9(6)
txn_type = rng.choice(["DR", "DR", "CR"]) # PIC X(2)
cents = rng.randint(1, 250_000_00)
amount = f"{cents:011d}" # PIC 9(9)V99: cents, no point
date = f"2026{month:02d}{day:02d}" # PIC 9(8)
memo = rng.choice(MEMOS)[:29].ljust(29) # PIC X(29): space padded on the right
Numeric fields are zero padded on the left and text fields are space padded on the right, because that is how a COBOL MOVE would fill them and how a COBOL READ expects to find them. The file is opened with newline="\n" so every record ends in a single line feed. The Programmer's Guide describes a LINE SEQUENTIAL file as records terminated by a line feed on Unix builds or a carriage return plus line feed on native Windows builds, and says that short records are padded with spaces on read and long ones truncated. Writing a bare line feed is the safe choice for a file that will be read on either kind of build.
The script then reads its own output back with a struct.Struct("8s6s2s11s8s29s"), which is the Python spelling of the copybook, and sums debits and credits in integer cents. Summing cents rather than floats is deliberate: COBOL does exact decimal arithmetic on 9(9)V99 fields, and the Python side should match it exactly rather than approximately.
The COBOL reader
>>SOURCE FORMAT IS FREE
*> python-fixed-width.cob — read a Python-generated
*> fixed-width transaction file and print control totals
*> Dialect: GnuCOBOL 3.x, free format
*> Status : desk-checked against the GnuCOBOL 3.2 Programmer's Guide;
*> compiled and run with GnuCOBOL 3.2 on 2026-10-29
*> Inputs : datasets/python-fixed-width-test-data-cobol.dat
*> (LINE SEQUENTIAL, LF line endings, 64 bytes per record)
*> Outputs: control totals on standard output
*> Build : cobc -x -I code code/python-fixed-width.cob
*> Run : ./python-fixed-width (from the site folder)
IDENTIFICATION DIVISION.
PROGRAM-ID. FIXED-WIDTH-READER.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT Txn-File
ASSIGN TO "datasets/python-fixed-width-test-data-cobol.dat"
ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-Status.
DATA DIVISION.
FILE SECTION.
FD Txn-File.
COPY "python-fixed-width-test-data-cobol.cpy".
WORKING-STORAGE SECTION.
01 WS-Status PIC X(2).
01 WS-Eof-Flag PIC X VALUE "N".
88 End-Of-File VALUE "Y".
01 WS-Counters.
05 WS-Read-Count PIC 9(6) VALUE 0.
05 WS-Debit-Count PIC 9(6) VALUE 0.
05 WS-Credit-Count PIC 9(6) VALUE 0.
05 WS-Bad-Count PIC 9(6) VALUE 0.
01 WS-Totals.
05 WS-Debit-Total PIC 9(11)V99 VALUE 0.
05 WS-Credit-Total PIC 9(11)V99 VALUE 0.
05 WS-Net PIC S9(11)V99 VALUE 0.
01 WS-Edited.
05 WS-Count-Out PIC ZZZ,ZZ9.
05 WS-Amount-Out PIC Z,ZZZ,ZZZ,ZZ9.99.
05 WS-Net-Out PIC -,ZZZ,ZZZ,ZZ9.99.
PROCEDURE DIVISION.
Main-Paragraph.
OPEN INPUT Txn-File
IF WS-Status NOT = "00"
DISPLAY "Cannot open input file, status " WS-Status
STOP RUN
END-IF
PERFORM UNTIL End-Of-File
READ Txn-File
AT END
SET End-Of-File TO TRUE
NOT AT END
PERFORM Process-Record
END-READ
END-PERFORM
CLOSE Txn-File
PERFORM Print-Totals
STOP RUN.
Process-Record.
ADD 1 TO WS-Read-Count
IF Txn-Amount IS NOT NUMERIC
ADD 1 TO WS-Bad-Count
DISPLAY "Bad amount in transaction " Txn-Id
ELSE
EVALUATE TRUE
WHEN Txn-Debit
ADD 1 TO WS-Debit-Count
ADD Txn-Amount TO WS-Debit-Total
WHEN Txn-Credit
ADD 1 TO WS-Credit-Count
ADD Txn-Amount TO WS-Credit-Total
WHEN OTHER
ADD 1 TO WS-Bad-Count
DISPLAY "Bad type in transaction " Txn-Id
END-EVALUATE
END-IF.
Print-Totals.
MOVE WS-Read-Count TO WS-Count-Out
DISPLAY "Records read : " WS-Count-Out
MOVE WS-Debit-Count TO WS-Count-Out
MOVE WS-Debit-Total TO WS-Amount-Out
DISPLAY "Debits : " WS-Count-Out " " WS-Amount-Out
MOVE WS-Credit-Count TO WS-Count-Out
MOVE WS-Credit-Total TO WS-Amount-Out
DISPLAY "Credits : " WS-Count-Out " " WS-Amount-Out
MOVE WS-Bad-Count TO WS-Count-Out
DISPLAY "Rejected : " WS-Count-Out
COMPUTE WS-Net = WS-Credit-Total - WS-Debit-Total
MOVE WS-Net TO WS-Net-Out
DISPLAY "Net (CR - DR) : " WS-Net-Out.
Line by line
The first line switches the compiler to free format; it sits in column 8 so that it is also legal if the compiler starts in its default fixed mode. The SELECT names the data file relative to the folder the program is run from and asks for a two-character FILE STATUS, which the Programmer's Guide lists as returning 00 on success, 10 at end of file, and 35 when the file is not found.
The FD is followed by a COPY statement instead of a record description. The compiler inserts the copybook text before compilation begins, so Txn-Record and its fields exist exactly as if they had been typed here. The -I code option on the build line tells cobc where to look for the copybook.
The main loop is the standard read-until-end pattern from the sequential file lesson: an inline PERFORM whose UNTIL condition is the level-88 End-Of-File, set by the AT END branch of READ. The IBM reference and the Programmer's Guide both describe AT END as the branch taken when no further record exists, and NOT AT END as the branch taken on a successful read.
Process-Record does the one thing a fixed-width reader must never skip. Before it adds Txn-Amount to anything, it applies the NUMERIC class test. The Programmer's Guide is strict about what passes: only the digit characters, with spaces, decimal points and signs all failing. If someone regenerates the file with a decimal point in the amount, the record is counted as rejected and named on the console, and the totals stay honest. The EVALUATE TRUE then routes the record by its condition name, with WHEN OTHER catching a type that is neither DR nor CR.
Print-Totals moves each counter and total into a numeric-edited field before displaying it. The Z symbols suppress leading zeros, the commas are inserted only where a digit precedes them, and the single minus at the front of WS-Net-Out is a fixed sign: it always takes the leftmost position, a space when the net is positive and a minus when it is negative, with the suppressed zeros left as spaces between it and the first digit. To make the sign float against the digits, the picture would repeat it, as in -,---,---,--9.99. The edited fields are formatted the way the Python script formats its own output, so the two printouts can be compared line for line.
Compile and run
python code/python-fixed-width-test-data-cobol.py
cobc -x -I code code/python-fixed-width.cob
./python-fixed-width
Run the three commands from the site folder, because the COBOL program opens datasets/python-fixed-width-test-data-cobol.dat by that relative path. The directive on line 1 makes -free unnecessary, though adding it does no harm. On Windows the executable is python-fixed-width.exe.
Worked example
The Python run on this machine produced the following, and the compiled COBOL program printed the same five lines after its own read of the file.
Records read : 200
Debits : 129 16,031,608.02
Credits : 71 9,583,778.88
Rejected : 0
Net (CR - DR) : - 6,447,829.14
Two checks are worth doing by hand. The debit and credit counts add up to the 200 records read, so nothing was rejected and nothing was miscounted. And the net is credits minus debits, which is negative here because the generator picks DR twice as often as CR, so the minus in the last line is doing real work.
The first record of the file, read with od -c, is 00000001 100262 DR 00024065539 20260919 Cash withdrawal followed by spaces to column 64 and a line feed. That is a debit of 240,655.39 on account 100262, and the eleven amount digits with no point are what the V in the copybook expects.
Limitations
The listing was compiled and run with GnuCOBOL 3.2 only, and the totals were first computed by the Python parser; the two agree. The file uses ASCII only; a memo containing a multibyte character would occupy more than one byte and shift every field after it, which is the classic fixed-width failure. The generator writes line feeds only, which GnuCOBOL reads on both Unix and Windows builds, but a file produced by a native Windows program would carry carriage returns and should be checked with od -c before it is trusted. The NUMERIC test guards the amount but not the date, so a malformed date would pass through to the memo unnoticed; adding Txn-Date IS NOT NUMERIC to the validation is a two-line change. Finally, PIC 9(11)V99 totals overflow silently at a hundred billion, a limit this file cannot reach but a production feed could, and the lesson on ROUNDED and ON SIZE ERROR shows how to make that failure loud.
Correction, 29 October 2026: the net line was shown as -6,447,829.14, with the minus against the first digit. The picture -,ZZZ,ZZZ,ZZ9.99 has a single minus, which is a fixed sign in the leftmost position, and a real GnuCOBOL 3.2 run prints - 6,447,829.14; the line, the explanation of the picture and the Python companion's formatting are corrected. The build command shown before also named the source python-fixed-width-test-data-cobol.cob, and GnuCOBOL 3.2's cobc refuses a source file whose base name is longer than 31 characters ("invalid file base name ... length exceeds maximum"), even with -o, so it failed as printed; the listing is now shipped as python-fixed-width.cob, and the old download address redirects there. With the line corrected, the output shown matches a real run line for line, so the page now says the program was compiled and run.
Index terms cobol, gnucobol, python, fixed-width, copybook