CLOSE
megamenu-tech
CLOSE
service-image
CLOSE
CLOSE
Blogs
From Green Screens to Python: A Structured Playbook for Modernizing an AS/400 Codebase

Technology

From Green Screens to Python: A Structured Playbook for Modernizing an AS/400 Codebase

#Best Practices

#Business

#Data & AI

#Enterprise Data

#Generative AI

#LLM

#Product Strategy

By

July 30, 2026

Post 1

Why This Problem Refuses to Go Away

The AS/400 — later rebranded iSeries, System i, and today IBM i on Power — is one of the most durable platforms ever built. Banks, insurers, manufacturers, distributors, and logistics companies still run mission-critical workloads on RPG and COBOL programmes that were written in the 1990s and never stopped working. That reliability is exactly the trap: the system works, so it never gets touched, and the knowledge to maintain it retires with the people who wrote it.

The pressure to move is rarely about the hardware. It is about people, integration, and speed:

  • The RPG/CL talent pool is shrinking every year — the remaining developers are expensive, hard to replace, and approaching retirement age.
  • Green-screen 5250 applications cannot participate in modern REST/event-driven ecosystems without brittle middleware that adds cost and fragility every time a new integration is required.
  • Business logic is buried in tens of thousands of lines of fixed-format RPG with no tests, no documentation, and heavy coupling between UI, data access, and calculation logic.
  • Every new integration costs more than it should — a mobile app, a partner API, an analytics pipeline. Each one requires someone who understands the AS/400's data conventions and calling patterns.

Moving to Python is attractive because it gives you a large talent pool, a mature web and data ecosystem (Django, FastAPI, SQLAlchemy, Celery, pandas), and first-class cloud support. But a naive rewrite is where most legacy modernization projects die. This guide lays out a structured, phased approach that de-risks the move — with concrete code showing how RPG and DDS constructs map to idiomatic Python, and how Reckonsys uses Claude Code to run these programmes faster and more safely than was possible even two years ago.

What You Are Actually Migrating: The AS/400 Building Blocks

Before writing a line of Python, it helps to be precise about the AS/400 components, because each one maps to a different modern concern — and treating them as a single undifferentiated 'legacy system' is how migration projects scope incorrectly from day one.

AS/400 Concept  What It Is  Modern Equivalent 
RPG (RPG III / IV / RPGLE)  Business logic. Older code is fixed-format (columns matter); newer code is free-format. A single program often mixes screen handling, file I/O, and calculations in one monolith.  Python service layer — pure functions/classes, no I/O, unit-testable 
DDS Physical File (PF)  Your schema. Tables defined via Data Description Specifications — packed/zoned decimal fields, fixed-width characters, date-as-number conventions.  PostgreSQL table + SQLAlchemy or Django ORM model 
DDS Logical File (LF)  Views and access paths (indexes) over physical files. May apply filters, reorder fields, define alternate key sequences.  PostgreSQL index, SQL view, or ORM query 
DDS Display File (DSPF)  The green screens. Defines the 5250 terminal interface — field positions, validation, function keys.  REST API (FastAPI/DRF) + React or equivalent frontend 
CL (Control Language)  Scripting and orchestration — job scheduling, calling programmes, file overrides, batch flows.  Python CLI / Celery task / Apache Airflow DAG 
DB2 for i  The integrated relational database. Data already lives in a real RDBMS — which is a gift. Migration is a data-and-logic problem, not a file-format reverse-engineering problem.  PostgreSQL (or DB2 LUW during transition) 
Packed/zoned decimal  Exact-precision numeric type used for money and quantities. Never equivalent to a floating-point type.  Python decimal.Decimal — non-negotiable 
CHAIN / READ (record-level access)  RPG's primary data access idioms — keyed lookup and sequential cursor read.  ORM get() / queryset iteration, or set-based SQL query 

The Target Architecture

The end state we design toward is a conventional, cloud-friendly Python stack. The golden rule: separate the three things RPG glued together — presentation, business logic, and data access — so each can evolve independently.

Layer  Technology  Replaces 
API layer  FastAPI or Django REST Framework — REST endpoints, optionally GraphQL or event streams  DDS Display Files (DSPF) and their 5250 terminal interaction model 
Service layer  Pure Python modules — no framework, no I/O, fully unit-testable  RPG business logic — calculations, validations, rules 
Data layer  PostgreSQL + SQLAlchemy or Django ORM; Decimal for all monetary fields  DB2 for i + DDS Physical Files 
Batch/orchestration  Celery + Redis, or Apache Airflow  CL programmes and IBM i job scheduler 
Frontend  React or equivalent modern web framework  5250 green screens 
Infrastructure  Containerised on AWS (ECS/EKS), CI/CD, observability, IaC  IBM i Power hardware and proprietary job management 

A Structured, Phased Approach: Six Phases That Make Migration Safe

Modernization fails when it is treated as a single heroic rewrite. It succeeds when it is an incremental, measurable programme where every step is reversible and the business keeps running on proven code at all times.

Phase 0 — Discovery, Assessment, and Business Case

You cannot migrate what you cannot see. The first phase is an inventory and a decision framework, not code. The goal is a dependency map, a risk-ranked migration backlog, a target architecture, and a business case with sequencing — all before a line of Python is written.

Cataloguing programmes, files, and jobs can be substantially automated by querying IBM i system catalogs rather than reading source by hand:

-- Objects by type in a library

SELECT OBJTYPE, COUNT(*) AS CNT

FROM TABLE(QSYS2.OBJECT_STATISTICS('APPLIB', '*ALL')) X

GROUP BY OBJTYPE ORDER BY CNT DESC;

-- Programme-to-file and programme-to-programme dependencies

SELECT PROGRAM_NAME, OBJECT_NAME, OBJECT_TYPE

FROM QSYS2.PROGRAM_INFO

WHERE PROGRAM_LIBRARY = 'APPLIB';

For each module, apply the 6 R's decision framework: Retire, Retain, Rehost, Replatform, Refactor, or Rewrite/Rearchitect. Not everything should become Python. Some programmes are dead, some are pure lift-and-shift, and only the high-value business logic deserves a careful refactor.

✓ Phase 0 Output: Dependency map, risk-ranked migration backlog with 6-R decisions, target architecture document, and a costed, sequenced roadmap.

Phase 1 — Choose a Migration Strategy (and Default to the Strangler Fig)

There are three broad strategies. Only one of them is safe for anything non-trivial.

Strategy  How It Works  Risk Profile 
Big Bang  Rewrite everything, cut over once. Business runs on unproven code from day one.  Highest risk. Avoid for anything non-trivial. One undiscovered edge case in a financial calculation affects every transaction. 
Parallel Run  New and old systems run side by side on the same inputs. Outputs compared until confidence is high.  High validation confidence. Best for correctness-critical domains (finance, billing, insurance). Slower and more resource-intensive. 
Strangler Fig (recommended)  A façade sits in front of the legacy system. Migrate one capability at a time, routing each request to either the Python service or the AS/400. The old system is 'strangled' gradually.  Low risk at every step. Every change is reversible. The business runs on proven code until each capability is validated. The recommended default. 

The strangler façade is a thin routing layer — in FastAPI, it is approximately twenty lines. A capability moves into the 'migrated' set only when its Python implementation has passed parallel-run validation. Until then, it continues hitting the AS/400. This is what makes the programme safe: there is never a moment where the whole business depends on unproven code.

✓ Phase 1 Output: Migration strategy selected. Strangler façade deployed. Feature flag routing established between legacy AS/400 and new Python services.

Phase 2 — Migrate the Data Layer First

Data is the foundation. Move it before logic. DDS physical files become PostgreSQL tables, and three conventions must be handled correctly or the migration produces silent data corruption.

DDS Convention  The Risk If Ignored  Correct PostgreSQL / Python Handling 
Packed decimal (P) / Zoned decimal (S)  Mapping to FLOAT produces penny-level rounding errors across millions of rows. Silent. Fatal to reconciliation.  NUMERIC(n, d) in PostgreSQL. decimal.Decimal in Python ETL. No exceptions. 
Fixed-width character fields  VARCHAR fields carry trailing spaces from fixed-width padding if not trimmed. Data is technically present but corrupted for display and comparison.  rstrip() on extraction. Normalise empty-after-trim to None/NULL. 
Dates as packed integers (YYYYMMDD)  Load integer directly into a DATE column: load crashes or produces nonsense dates. Map sentinel value 0 to NULL without explicit handling: real-looking 1900-01-01 rows appear in production.  Explicit conversion function. Sentinel values (0, 99999999) map to NULL. Bad values raise and log — never silently dropped. 

The SQLAlchemy model for a customer master file illustrates the mapping directly — each column comment traces it to the original DDS field name, type, and decimal positions:

class Customer(Base):

__tablename__ = 'customer_master'

customer_id   = Column(Integer, primary_key=True)   # CUSTID  7P0

name          = Column(String(30), nullable=False)   # CUSTNM  30A

credit_limit  = Column(Numeric(11, 2), default=0)   # CRDLIMIT 9P2

balance       = Column(Numeric(11, 2), default=0)   # BALANCE  9P2

status        = Column(String(1), default='A')      # STATUS   1A

last_order_dt = Column(Date, nullable=True)          # LSTORDDT 8P0

Two rules that prevent silent data corruption on every load: always route money and quantities through Decimal, never float; and validate with row counts and control totals (sum of balances, min/max keys) between source and target after every load. A load that completes without error is not a successful migration. A load whose control totals match the source is.

✓ Phase 2 Output: PostgreSQL schema created. ETL scripts extract, transform, and load with correct decimal handling, character trimming, and date conversion. Control totals reconciled between DB2 for i source and PostgreSQL target.

Phase 3 — Translate the Business Logic

This is the heart of the project. The trap is translating RPG line by line. The goal is to preserve behaviour while producing clean, testable Python. The two things are different.

Fixed-format RPG mixes screen handling, file I/O, and calculation logic in a single monolith, with shared state communicated through indicator variables (*IN99, *IN50, and so on) that any subroutine can set and any other can read. That shared-state model is one of the biggest sources of legacy bugs. The Python translation replaces it with explicit return values and typed exceptions.

Consider an RPG programme that validates and applies an order against a customer's credit limit. It uses CHAIN (a keyed lookup), *IN99 (the not-found indicator), a series of guard checks, and an in-place UPDATE. The Python equivalent:

# services/credit.py

from dataclasses import dataclass

from decimal import Decimal

from datetime import date

class OrderError(Exception):

"""Raised when an order cannot be applied.

Message mirrors legacy MSG codes for parallel-run comparison."""

@dataclass(frozen=True)

class OrderResult:

customer_id: int

new_balance: Decimal

message: str = 'Order applied'

def apply_order(session, customer_id: int,

order_amount: Decimal,

today: date | None = None) -> OrderResult:

today = today or date.today()

customer = session.get(Customer, customer_id)   # CHAIN CUSTREC

if customer is None:                            # *IN99 = *ON

raise OrderError('Customer not found')

if customer.status != 'A':                     # STATUS <> 'A'

raise OrderError('Customer not active')

new_balance = customer.balance + order_amount   # EVAL NEWBAL

if new_balance > customer.credit_limit:         # IF NEWBAL > CRDLIMIT

raise OrderError('Over credit limit')

customer.balance = new_balance                  # UPDATE CUSTREC

customer.last_order_dt = today

session.commit()

return OrderResult(customer_id, new_balance)

Several deliberate upgrades happen in this translation. RPG's implicit shared state becomes explicit return values and exceptions. The *INxx indicator pattern — set an indicator and check it forty lines later — is eliminated. The keyed CHAIN becomes a primary-key get(). And because the function has no I/O dependencies, it can be tested in isolation:

def test_over_credit_limit(session, make_customer):

make_customer(id=1001, balance='900.00',

credit_limit='1000.00', status='A')

with pytest.raises(OrderError, match='Over credit limit'):

apply_order(session, 1001, Decimal('200.00'))

def test_happy_path(session, make_customer):

make_customer(id=1002, balance='100.00',

credit_limit='1000.00', status='A')

result = apply_order(session, 1002, Decimal('50.00'))

assert result.new_balance == Decimal('150.00')

For free-format RPGLE, which reads closer to a modern language, the mapping is even more direct. A DOW/READ loop that totals open orders for a customer — a SETLL to position the cursor, then a READE loop until end-of-file — collapses into a single set-based SQL expression:

def total_open_orders(session, customer_id: int) -> Decimal:

stmt = (

select(func.coalesce(func.sum(Order.amount), 0))

.where(Order.customer_id == customer_id,

Order.status == 'O')

)

return session.scalar(stmt) or Decimal('0')

The record-at-a-time cursor loop — the defining shape of RPG — is almost always replaceable by a single set-based SQL query. The resulting code is both faster and clearer. Recognising these idioms is where experience pays off, and it is why Reckonsys pairs RPG-literate analysts with Python engineers on translation work.

✓ Phase 3 Output: Python service functions for each translated programme. Characterization test suite covering normal paths, error paths, and boundary conditions. Every *INxx indicator replaced by an explicit return value or exception.

Phase 4 — Replace CL and Batch Jobs

CL programmes orchestrate: they override files, call programmes, and sequence nightly batch. Their modern equivalent is a Celery task chain or an Apache Airflow DAG — which adds retries, monitoring, alerting, and horizontal scale that the IBM i job scheduler never had.

A nightly CL programme that posts orders, ages receivables, and notifies the ops team becomes:

# tasks.py — Celery equivalent of the nightly CL sequence

@app.task(bind=True, max_retries=3, default_retry_delay=60)

def post_orders(self):

...   # translated POSTORDERS logic

@app.task

def age_receivables():

...   # translated AGERECV logic

def run_nightly():

chain(

post_orders.s(),

age_receivables.si(),

notify_ops.s()

).apply_async()

The key capability gains: retries with exponential backoff (no CL equivalent), structured logging and observability into each task's execution, and the ability to scale individual tasks horizontally if processing volume grows.

✓ Phase 4 Output: All CL job sequences replaced by Celery task chains or Airflow DAGs. Batch jobs monitored, retriable, and observable. Job scheduler dependency eliminated.

Phase 5 — Rebuild the Interface

Green-screen DSPF display files become a clean REST API plus a modern frontend. Each migrated capability is exposed through FastAPI or Django REST Framework with Pydantic input validation — including decimal precision validation that mirrors the original field definitions:

class OrderIn(BaseModel):

customer_id: int

amount: condecimal(max_digits=11, decimal_places=2)

@app.post('/orders/apply')

def apply(order: OrderIn, session=Depends(get_session)):

try:

result = apply_order(session, order.customer_id, order.amount)

except OrderError as e:

raise HTTPException(status_code=422, detail=str(e))

return {

'customer_id': result.customer_id,

'new_balance': str(result.new_balance),

'message': result.message

}

The React frontend consumes this API. The former 5250 workflow becomes a responsive web application — and, for the first time, the same business logic is reachable by mobile applications, partner integrations, and analytics pipelines without a middleware shim.

✓ Phase 5 Output: REST API endpoints deployed for each migrated capability. React frontend consuming the API. 5250 green-screen dependency eliminated for migrated capabilities.

Phase 6 — Parallel Run, Validation, and Cutover

Before any capability is trusted with production traffic, run it in shadow mode: send production inputs to both the AS/400 and the new Python service, compare outputs, and log every divergence. Only when the mismatch rate is effectively zero over a meaningful period does the capability's flag flip in the strangler gateway.

def shadow_apply_order(payload):

legacy = call_as400_service(payload)    # source of truth

new    = call_python_service(payload)   # new implementation

if legacy != new:

log_divergence(payload, legacy, new) # investigate before cutover

return legacy                            # legacy still authoritative

Cut over capability by capability. Keep the AS/400 available for rollback. Decommission legacy objects only once their Python replacements have carried real production traffic for a defined period. This discipline turns a scary migration into a series of small, reversible steps — each one provably correct before the next begins.

✓ Phase 6 Output: Shadow comparison harness operational. Mismatch rate at zero for each capability before cutover. AS/400 retained as read-only rollback. Legacy objects decommissioned only after sustained production validation.

How Claude Code Compresses the Timeline

Every phase above used to be gated by one scarce resource: a person who could read decades-old RPG and write modern Python. Claude Code — Anthropic's terminal-based agentic coding tool — changes that constraint significantly. Here is where it moves the needle in a modernisation programme.

Task  What Claude Code Does — and What It Does Not Replace  Task 
Comprehending undocumented legacy code  Point Claude Code at a directory of RPG and DDS source and ask it to explain a programme's business intent, inputs, outputs, and dependencies — including flagging *INxx indicator logic that hides shared state. What used to take weeks of archaeology takes hours. The output becomes the acceptance criteria for the Python rewrite.  Comprehending undocumented legacy code 
Automated inventory and dependency mapping  Claude Code can script the Phase 0 audit itself — running the QSYS2 catalog queries, parsing the output, and producing the dependency graph and 6-R backlog — rather than a human assembling spreadsheets by hand.  Automated inventory and dependency mapping 
Translation with a human in the loop  Ask it to translate a programme into a clean, tested Python service and it produces the service function and the pytest suite, following the patterns your team has established. A CLAUDE.md file at the repo root encodes those standards so every translation is consistent — Decimal always, no floats; business logic in services/ as pure functions; every *INxx indicator becomes an explicit return value.  Translation with a human in the loop 
Generating the characterisation test suite  Legacy code rarely has tests. Claude Code builds the characterisation test suite from sample inputs and outputs and parallel-run logs — so refactoring stays safe. This is particularly valuable because the test suite is what allows aggressive refactoring without fear of undetected regressions.  Generating the characterisation test suite 
Parallelising the work  For a large codebase, Claude Code can dispatch subagents to work through independent modules concurrently — one investigating batch and CL flows while another translates a cluster of order-management programmes — with a human reviewing each pull request.  Parallelising the work 
What stays human  Architectural judgement. The 6-R decisions. The parallel-run validation that proves correctness. Edge-case handling surfaced during profiling. Design sign-off. The AI accelerates the tedious 80%. Engineers own the design and the correctness.  What stays human 

Cross-Cutting Concerns You Cannot Skip

Concern  Why It Cannot Be Deferred  None
Numeric fidelity  Every packed and zoned decimal must land in Python Decimal with the correct scale. A single float in a financial calculation will eventually produce a penny error that fails reconciliation — and erodes trust in the whole programme. This is non-negotiable and has no exceptions.  None
Character encoding  Source data is EBCDIC and fixed-width. Decode to UTF-8 and trim padding on extraction. Test with real names containing special characters — particularly relevant for Indian names in multi-byte character ranges.  None
Concurrency semantics  RPG relies on record-level locking. Reproduce the intent with database transactions and, where needed, SELECT ... FOR UPDATE (row locks) or optimistic version columns. Do not assume the ORM default matches the old behaviour.  None
Test coverage as the safety net  Legacy code rarely has tests. Build a characterisation test suite from real inputs and outputs before refactoring. That suite, combined with the parallel run, is what lets you refactor aggressively without fear of silent regressions.  None
Do not migrate the mess  Dead programmes, unused files, and copy-pasted logic should be retired in Phase 0, not faithfully reproduced in Python. Modernisation is the one chance to pay down decades of accumulated technical debt. Take it.  None

How Reckonsys Approaches AS/400-to-Python Modernization

Reckonsys is a software engineering firm with deep capability in exactly the technologies this migration requires: Python, AWS, DevOps, data engineering, and QA automation. We structure modernization work to address the risks that sink most legacy programmes — and we use Claude Code to compress the timeline without surrendering control over correctness.

  • Discovery and Technical Consulting First

A Technical Consulting engagement runs the Phase 0 audit: automated inventory of programmes, files, and jobs; dependency mapping; and a risk-ranked, 6-R backlog with a costed roadmap. The output is a clear-eyed picture of what to retire, rehost, and refactor — before committing budget to code.

  • Strangler-Fig Delivery, Not Big-Bang Gambles

The strangler façade approach means the business keeps running on proven code at every step. Each capability moves to Python only after parallel-run validation confirms zero divergence. Every change is reversible until it is not.

  • AI-Accelerated, Human-Governed Delivery

Our engineers use Claude Code to read and document the legacy RPG, generate first-draft Python services and their tests, and build characterisation suites — then apply their own architectural judgement and review to every change. The result is dramatically faster translation without surrendering control over correctness, security, or design.

  • Python and Data Engineering Depth

Translated services are built on modern Python — FastAPI or Django, SQLAlchemy, Celery — with a dedicated data engineering practice handling the DB2 for i to PostgreSQL migration, packed-decimal and date normalisation, and control-total validation. This is covered in detail in Part 2 of this series.

  • Cloud and DevOps to Land It in Production

With AWS and DevOps as core competencies, we containerise the new services, set up CI/CD, observability, and infrastructure-as-code — so the modernised system is scalable, monitored, and cheaper to operate than the platform it replaced.

  • Flexible Engagement Models

Whether the right model is end-to-end delivery, staff augmentation with RPG-aware Python engineers embedded in your team, or technical consulting and architecture advisory — the engagement flexes to how your organisation wants to work.

Modernizing an AS/400 codebase to Python is not a rewrite. It is a disciplined, phased programme: understand the system, migrate the data, translate the logic into clean and tested services, replace batch and screens, and cut over one safe step at a time.

The AS/400's greatest strength — its decades of embedded business rules, accumulated through thousands of production edge cases — becomes your greatest asset when those rules are lifted into a well-tested Python service layer that the rest of your ecosystem can finally reach. Mobile apps, partner APIs, analytics pipelines, and AI systems can all call the same logic. For the first time.

What has changed is the cost curve. With Claude Code reading the legacy source, drafting translations, and generating the test safety net, the comprehension-and-translation work that once made these projects prohibitively slow is now measured in days, not quarters — while human engineers keep ownership of design and correctness.

The organisations that succeed treat modernization as risk management, not heroics. That is precisely the approach — AI-accelerated, human-governed, backed by Python, cloud, data, and QA depth — that Reckonsys brings to every AS/400 engagement. Conclusion

Reckonsys Tech Labs

Reckonsys Team

Authored by our in-house team of engineers, designers, and product strategists. We share our hands-on experience and practical insights from the front lines of digital product engineering.

Modal_img.max-3000x1500

Discover Next-Generation AI Solutions for Your Business!

Let's collaborate to turn your business challenges into AI-powered success stories.

Get Started