AI-Powered S/4HANA Migration Validator
Build a Python tool that validates SAP migration data and generates AI reports.
Introduction
30 Second Summary
Every S/4HANA migration reaches a point where someone has to compare legacy extracts against target loads, record by record, hunting for missing fields, duplicates, and data that failed to make it across. That manual spreadsheet validation is slow, error-prone, and impossible to repeat consistently across mock cycles.
In this project, you will build a Python command-line tool that automates those validation checks on your CSV data extracts and uses the Claude API to transform raw discrepancies into structured, consultant-grade migration reports.
What You'll Build
You'll point the tool at a pair of legacy and target CSVs, run one command, and get back a full migration validation report with an executive summary, critical findings with root cause analysis, and prioritized remediation steps ready to hand to a project manager.
By the end of this project, you'll have:
- A validation engine that catches the issues that block real go-lives: missing mandatory S/4HANA fields, duplicate records, count mismatches, and records lost in migration.
- An AI-powered analysis pipeline where Claude analyzes raw validation output and produces a report with executive summary, root cause analysis, and SAP-grounded remediation recommendations.
- A reusable framework you can adapt for real migration projects by swapping in your own legacy and target CSV extracts.
- Secret Mission: Extend the validator to handle Business Partner data with CVI-specific validation rules.
Are there any prerequisites?
No prior Python experience is needed. This guide covers setup for all required tools. You will need an Anthropic API key, which is free to create and charges only for API usage (pay-per-use).
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters for your next S/4HANA migration project.
Set Up Your Development Environment
Before you can automate any migration validation, you need three things in place: a code editor to write your Python scripts, Python itself to run them, and access to the Claude API to power the AI analysis.
In this step, you will install VS Code and Python, create an isolated project workspace, and connect to the Anthropic API by generating an API key.
In this step, get ready to:
- Install VS Code and Python 3.13.15.
- Create a project folder with a virtual environment.
- Install the anthropic and pandas packages and set up your API key.
Install VS Code and Python
VS Code is a free code editor with a built-in terminal, syntax highlighting, and extensions for Python development. Python is the language you will use to build the validation tool.
- Open your web browser and go to code.visualstudio.com.
- Click the Download for Windows button.
- Run the downloaded installer and follow the prompts to complete the installation.
Now download and install Python 3.13.15.
- Go to the Python 3.13.15 release page.
- Scroll down to the Files section.
- Click Windows installer (64-bit) to download it.
- Run the downloaded installer.
- Check the Add Python to PATH checkbox at the bottom of the installer window.
- Click Install Now.
Do not skip the PATH checkbox
The Add Python to PATH checkbox is unchecked by default. If you skip it, your terminal will not be able to find the python command later. Make sure it is checked before you click Install Now.
- Press the Windows key to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open a terminal inside VS Code by pressing Ctrl+` (the backtick key, above Tab).
- Check your Python installation by running:
python --version
✔️ I see Python 3.13.15
Python is installed and on your PATH. You are ready to continue.
ⓧ I see a different version
You have a different version of Python installed. This project requires 3.13.15 for compatibility with the packages you will install.
- Go to the Python 3.13.15 release page and download the Windows installer (64-bit).
- Run the installer. Check Add Python to PATH and click Install Now. The new version replaces the old one.
- Close and reopen VS Code entirely, then run python --version again in a new terminal.
ⓧ 'python' is not recognized
Python is not on your system PATH. This usually means the Add Python to PATH checkbox was not checked during installation.
- Rerun the Python installer you downloaded.
- Select Modify and make sure Add Python to PATH is checked.
- Close and reopen VS Code entirely (not just the terminal), then run python --version again.
- Open the Extensions view by pressing Ctrl+Shift+X.
- Type Python in the search bar.
- Find the Python extension by Microsoft and click Install.
What does the Python extension do?
The Python extension adds syntax highlighting, code completion, and automatic virtual environment detection to VS Code. It also lets VS Code find the correct Python interpreter for your project.
Create your project folder and virtual environment
A virtual environment keeps your project's packages separate from other Python projects on your machine. This prevents version conflicts between different projects.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir s4hana-validator
cd s4hana-validator
- Create and activate a virtual environment by running:
python -m venv venv
venv\Scripts\activate
What do these commands do?
- mkdir s4hana-validator creates a new folder called s4hana-validator on your Desktop.
- python -m venv venv creates an isolated Python environment inside a venv subfolder.
- venv\Scripts\activate activates the virtual environment so that packages you install go into this project only.
Your terminal prompt should now start with (venv). This confirms the virtual environment is active.
Don't see (venv) in your prompt?
- If you see a red error about running scripts being disabled, PowerShell is blocking the activation script. Run the following command, then try venv\Scripts\activate again:
- Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser.
- Make sure you are inside the s4hana-validator folder. Run cd ~/Desktop/s4hana-validator to get there.
- help me activate my Python virtual environment on Windows
Install packages and set up your API key
The validator uses two Python packages: pandas for reading and comparing CSV data, and anthropic for sending validation findings to Claude for analysis.
- Install both packages by running:
pip install anthropic pandas
You should see output ending with a line confirming both packages were successfully installed.
Seeing a pip error?
- Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt.
- If you see a network error, check your internet connection and try running the command again.
- help me fix a pip install error for anthropic and pandas
Now you need an API key so your Python script can send data to Claude for analysis. Anthropic provides a pay-per-use API. The total cost for this entire project is under $0.05.
- Go to platform.claude.com/settings/keys in your browser.
- Create an Anthropic account if you do not have one yet.
- Click Create Key.
- Give your key a name like s4hana-validator and click Create Key.
- Copy the key immediately. You will not be able to see it again after closing the dialog.
Save your API key somewhere safe
Anthropic only shows your API key once at creation. Paste it into a text file or password manager right away. If you lose it, you will need to create a new one.
- Back in VS Code, click File in the top menu bar.
- Click New File.
- Paste the following line into the new file, replacing your-api-key-here with the API key you just copied:
ANTHROPIC_API_KEY=your-api-key-here
- Press Ctrl+S to save. A file dialog opens.
- Navigate to your s4hana-validator folder on the Desktop.
- Type .env as the filename and click Save.
Why store the key in a .env file?
The .env file keeps your API key separate from your code. This prevents you from accidentally sharing credentials when you share your scripts with teammates.
✔️ Awesome, I've got everything!
Make sure you have saved your .env file with your actual API key in the s4hana-validator folder.
ⓧ I'd like to double check the full code
Your .env file should contain exactly one line:
ANTHROPIC_API_KEY=your-api-key-here
Replace your-api-key-here with the actual key you copied from the Anthropic console.
Everything is installed. Before you run the final check, what do you think will happen when Python tries to import the anthropic package?
- Confirm Python is working by running:
python --version
You should see Python 3.13.15 printed.
- Confirm the Anthropic SDK is installed by running:
python -c "import anthropic; print('Claude SDK ready')"
You should see Claude SDK ready printed in the terminal. This proves Python, your virtual environment, and the Anthropic SDK are all working together.
Seeing ModuleNotFoundError?
- Check that your virtual environment is still active. Look for (venv) in your terminal prompt. If it is missing, run venv\Scripts\activate again.
- If the environment is active but the import still fails, rerun pip install anthropic pandas to reinstall both packages.
- help me fix a ModuleNotFoundError for anthropic in Python
Your development environment is ready. Next up, you will create realistic sample migration data with deliberate issues baked in so the validator has real problems to find.
Create Sample Migration Data
Your development environment is set up and ready. Now you need data to validate.
In a real S/4HANA migration, you would export data from your legacy ECC6 system and load it into S/4HANA via the Migration Cockpit. In this step, you will generate sample GL Account extracts with deliberate migration issues baked in so your validator has real problems to find.
In this step, get ready to:
- Create a Python script that generates legacy and target GL Account CSVs with realistic SAP field structures.
- Run the script and inspect the output to understand the data before building validation rules.
Create the legacy ECC6 GL account extract
The script you are about to build generates two CSVs that mimic real SAP data extracts. The legacy file represents a clean ECC6 export with 15 GL accounts. The target file represents an S/4HANA load with four deliberate issues embedded.
- Create a new file called create_sample_data.py in your s4hana-validator folder by right-clicking in the VS Code Explorer sidebar and selecting New File.
- Start building the script by adding the imports, the function definition, and the first three columns of the legacy GL account DataFrame. Copy the following code into the file:
import pandas as pd
import os
def create_sample_data():
os.makedirs("data", exist_ok=True)
legacy_gl = pd.DataFrame({
"SAKNR": [
"100000", "110000", "120000", "130000", "140000",
"200000", "210000", "220000", "300000", "310000",
"400000", "410000", "500000", "510000", "600000"
],
"BUKRS": [
"1000", "1000", "1000", "1000", "1000",
"1000", "1000", "1000", "1000", "1000",
"1000", "1000", "1000", "1000", "1000"
],
"TXT20": [
"Cash at Bank", "Petty Cash", "Accounts Recv",
"Inventory Raw", "Fixed Assets", "Accounts Pay",
"Accrued Liab", "Notes Payable", "Share Capital",
"Retained Earn", "Revenue Sales", "Rev Services",
"COGS Material", "COGS Labor", "SGA Expenses"
],
What are these SAP field names?
These are standard SAP field names you will recognise from your ECC work. SAKNR is the GL account number, BUKRS is the company code, and TXT20 is the short text description.
The pandas library's pd.DataFrame() takes a dictionary where each key is a column name and each value is a list of row data. This is similar to how you would structure a table in a spreadsheet.
- Continue building the legacy DataFrame by adding the remaining five columns directly below the code you just pasted. These cover the long text, account group, balance sheet indicator, P&L type, and currency:
"TXT50": [
"Cash at Bank - Main Account", "Petty Cash - Office",
"Accounts Receivable - Trade", "Inventory - Raw Materials",
"Fixed Assets - Equipment", "Accounts Payable - Trade",
"Accrued Liabilities", "Notes Payable - Short Term",
"Share Capital - Ordinary", "Retained Earnings",
"Revenue from Sales", "Revenue from Services",
"COGS - Direct Material", "COGS - Direct Labor",
"Selling General Admin"
],
"KTOKS": [
"SAKO", "SAKO", "SAKO", "SAKO", "SAKO",
"SAKO", "SAKO", "SAKO", "SAKO", "SAKO",
"SAKO", "SAKO", "SAKO", "SAKO", "SAKO"
],
"XBILK": [
"X", "X", "X", "X", "X",
"X", "X", "X", "X", "X",
"", "", "", "", ""
],
"GVTYP": [
"", "", "", "", "",
"", "", "", "", "",
"S", "S", "S", "S", "S"
],
"WAERS": [
"USD", "USD", "USD", "USD", "USD",
"USD", "USD", "USD", "USD", "USD",
"USD", "USD", "USD", "USD", "USD"
]
})
What do these remaining fields mean?
KTOKS is the account group (all set to SAKO for GL accounts). XBILK marks balance sheet accounts with an X. GVTYP indicates P&L statement type (S for P&L accounts). WAERS is the currency key.
Notice that all 15 legacy records are clean. Every field is populated correctly. This represents the ideal state of your ECC6 source extract.
Add the target S/4HANA extract with deliberate migration issues
The target extract simulates what you would see after running a Migration Cockpit load. It has the same field structure as the legacy, plus the S/4HANA-mandatory FSTAG (Field Status Group) column. Four deliberate issues are embedded for the validator to catch.
- Add the first three columns of the target DataFrame directly below the legacy DataFrame's closing }). Leave a blank line between them:
target_gl = pd.DataFrame({
"SAKNR": [
"100000", "110000", "120000", "130000", "140000",
"200000", "210000", "220000", "300000", "310000",
"400000", "410000", "500000", "510000",
"210000"
],
"BUKRS": [
"1000", "1000", "1000", "1000", "1000",
"1000", "1000", "1000", "1000", "1000",
"1000", "1000", "1000", "1000",
"1000"
],
"TXT20": [
"Cash at Bank", "Petty Cash", "Accounts Recv",
"Inventory Raw", "Fixed Assets", "Accounts Pay",
"Accrued Liab", "Notes Payable", "Share Capital",
"Retained Earn", "Revenue Sales", "Rev Services",
"COGS Material", "COGS Labor",
"Accrued Liab"
],
Can you spot the issues already?
Look carefully at the SAKNR column. Account 600000 (SGA Expenses) is missing from the target. Account 210000 (Accrued Liab) appears twice. These are exactly the kinds of issues that block real go-lives.
- Add the next three columns of the target DataFrame directly below:
"TXT50": [
"Cash at Bank - Main Account", "Petty Cash - Office",
"Accounts Receivable - Trade", "Inventory - Raw Materials",
"Fixed Assets - Equipment", "Accounts Payable - Trade",
"Accrued Liabilities", "Notes Payable - Short Term",
"Share Capital - Ordinary", "Retained Earnings",
"Revenue from Sales", "Revenue from Services",
"COGS - Direct Material", "COGS - Direct Labor",
"Accrued Liabilities"
],
"KTOKS": [
"SAKO", "SAKO", "SAKO", "SAKO", "SAKO",
"SAKO", "SAKO", "SAKO", "SAKO", "SAKO",
"SAKO", "SAKO", "SAKO", "",
"SAKO"
],
"XBILK": [
"X", "X", "X", "X", "X",
"X", "X", "X", "X", "X",
"", "", "", "",
"X"
],
- Finish the target DataFrame by adding the final three columns, the CSV write operations, the print statements, and the __main__ block directly below:
"GVTYP": [
"", "", "", "", "",
"", "", "", "", "",
"S", "S", "S", "S",
""
],
"WAERS": [
"USD", "USD", "USD", "USD", "USD",
"USD", "USD", "USD", "USD", "USD",
"USD", "USD", "USD", "",
"USD"
],
"FSTAG": [
"G001", "G001", "G001", "G001", "G001",
"G001", "G001", "G001", "G001", "G001",
"G001", "G001", "", "G001",
"G001"
]
})
legacy_gl.to_csv("data/legacy_gl_accounts.csv", index=False)
target_gl.to_csv("data/target_gl_accounts.csv", index=False)
print(f"Created legacy GL accounts: {len(legacy_gl)} records")
print(f"Created target GL accounts: {len(target_gl)} records")
print("Files saved to data/ folder")
if __name__ == "__main__":
create_sample_data()
Where are the four deliberate issues?
The target extract has four issues that mirror real migration failures:
- Missing record: Account 600000 (SGA Expenses) did not migrate to the target.
- Duplicate record: Account 210000 appears twice in the target, which would break posting logic.
- Missing mandatory fields: Account 510000 is missing KTOKS and WAERS. Account 500000 is missing FSTAG.
- New S/4HANA-mandatory column: FSTAG (Field Status Group) exists in the target but not in the legacy extract, reflecting S/4HANA's stricter field requirements.
- Save create_sample_data.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved create_sample_data.py before moving on.
ⓧ I'd like to double check the full code
The full file is too long to show in one block. Scroll up and confirm each section is present in your create_sample_data.py file, in this order:
- Chunk 1: import pandas as pd, import os, the create_sample_data() function definition, os.makedirs("data", exist_ok=True), and legacy_gl columns SAKNR through TXT20.
- Chunk 2: legacy_gl columns TXT50 through WAERS, closing with }).
- Chunk 3: target_gl columns SAKNR through TXT20 (15 rows including the duplicate 210000).
- Chunk 4: target_gl columns TXT50 through XBILK (with the blank KTOKS on account 510000).
- Chunk 5: target_gl columns GVTYP through FSTAG, closing with }), then the .to_csv() calls, print statements, and if __name__ == "__main__" block.
Run the script and inspect the output
Before you run the script, think about what you expect to happen. The script creates a data/ folder and writes two CSVs into it. What record counts should you see printed?
- Run the script in your VS Code terminal by running this command:
python create_sample_data.py
You should see three lines of output confirming 15 legacy records and 15 target records were created, and the files were saved to the data/ folder.
Why does the target also show 15 records?
The legacy has 15 unique accounts. The target is missing account 600000 but has a duplicate of account 210000, so the row count is still 15. A simple row count check alone would show PASS. That is exactly why automated validation needs multiple rules beyond just counting records.
Seeing a SyntaxError or IndentationError?
The most common cause is misaligned indentation between the chunks you pasted. Each column definition inside the DataFrame must be indented by 8 spaces (4 for the function, 4 for the dictionary).
Also check that you have the closing }) after each DataFrame and that commas separate every column definition.
Help me debug a SyntaxError in my create_sample_data.py file.
- Confirm the data files exist by running:
ls data/
You should see legacy_gl_accounts.csv and target_gl_accounts.csv listed.
- Click on data/legacy_gl_accounts.csv in the VS Code Explorer sidebar to open it.
- Scan the rows. You should see 15 accounts from 100000 through 600000, all under company code 1000, with every field populated.
- Now open data/target_gl_accounts.csv and look for the differences. You should see the extra FSTAG column, empty fields on some rows, and account 210000 appearing twice.
Why CSV instead of a direct database connection?
CSV files keep this project focused on validation logic rather than database connectivity setup. In a real project, you could swap pd.read_csv() for pd.read_excel() to read Excel exports or use a database connector to pull directly from SAP. The validation logic stays the same regardless of the data source.
Your sample migration data is ready with four realistic issues embedded. Next up, you will build the validation engine that automatically catches every one of these problems.
Build the Validation Engine
Your sample data is ready with realistic migration issues baked in. Now you need a way to find those issues automatically.
Checking migration data by hand in spreadsheets works for small loads. It falls apart when you have thousands of records across multiple migration cycles. In this step, you will build an automated validation engine that runs four checks drawn from real S/4HANA migration best practices.
In this step, get ready to:
- Write data loading and record count validation functions.
- Build mandatory field, duplicate, and missing record checks.
- Run all validations and review the raw findings.
Create the validator and check record counts
The first validation rule is the simplest and most fundamental: do the record counts match between legacy and target? If they do not, records were lost or duplicated during migration.
- Create a new file called migration_validator.py in your s4hana-validator folder. Start it with the imports and data loading function by pasting this code:
import pandas as pd
import anthropic
import json
def load_data(legacy_path, target_path):
# Read CSV files preserving leading zeros in SAP account numbers
legacy_df = pd.read_csv(legacy_path, dtype=str).fillna("")
target_df = pd.read_csv(target_path, dtype=str).fillna("")
return legacy_df, target_df
What does this code do?
The load_data function reads both CSV files with dtype=str, which forces pandas to treat every column as text. This is critical because SAP account numbers like 0000001000 have leading zeros that would be stripped if pandas read them as integers.
The .fillna("") call converts any blank cells from pandas' default NaN to empty strings. This makes it easier to check for missing values later. The anthropic and json imports will be used in the next step when Claude analyzes your findings.
- Add the record count check function and a quick test block below load_data by pasting this code at the bottom of the file:
def check_record_counts(legacy_df, target_df):
# Compare total records between legacy and target extracts
legacy_count = len(legacy_df)
target_count = len(target_df)
return {
"rule": "Record Count Reconciliation",
"legacy_count": legacy_count,
"target_count": target_count,
"status": "PASS" if legacy_count == target_count else "FAIL",
"difference": target_count - legacy_count
}
# Temporary test block - you will replace this later
if __name__ == "__main__":
legacy_df, target_df = load_data(
"data/legacy_gl_accounts.csv",
"data/target_gl_accounts.csv"
)
print(check_record_counts(legacy_df, target_df))
What does this check do?
In a healthy migration, the number of records in the legacy extract should match the target extract exactly. check_record_counts compares the two counts and returns PASS if they match or FAIL if they differ.
The test block at the bottom loads your sample data and prints the record count result. This is temporary testing code that you will replace with the full validation pipeline later.
- Save migration_validator.py.
- Run the validator by entering this command in your terminal:
python migration_validator.py
You should see a dictionary printed with status set to PASS and both counts at 15.
Why does the record count pass?
Both files have 15 records, so the counts match. But you know from the sample data that account 600000 is missing from the target and account 210000 appears twice. The missing record and the duplicate cancel each other out.
This is exactly why record count reconciliation alone is never enough. You need the additional checks you are about to build.
Seeing a ModuleNotFoundError?
- Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt. If not, run venv\Scripts\activate and try again.
- If the error mentions pandas or anthropic, re-run pip install anthropic pandas with your virtual environment active.
- help me debug a ModuleNotFoundError in my migration validator
Add mandatory field, duplicate, and missing record checks
Record counts give you a surface-level check. The next three functions dig deeper into the data itself: are required fields populated, are there duplicate keys, and did every legacy record make it across?
- In migration_validator.py, add the mandatory field check function below check_record_counts and above the if __name__ block:
def check_mandatory_fields(target_df, mandatory_fields):
# Check that S/4HANA-required fields are populated
findings = {
"rule": "Mandatory Field Completeness",
"details": []
}
for field in mandatory_fields:
if field in target_df.columns:
# Find records where the mandatory field is blank
missing = target_df[target_df[field] == ""]
if len(missing) > 0:
for _, row in missing.iterrows():
findings["details"].append({
"field": field,
"account": row.get("SAKNR", "Unknown"),
"company_code": row.get("BUKRS", "Unknown"),
"status": "FAIL"
})
findings["status"] = "FAIL" if findings["details"] else "PASS"
return findings
What does this check catch?
S/4HANA enforces stricter field requirements than ECC. Fields like FSTAG (Field Status Group) did not exist in ECC but are mandatory in S/4HANA. Records migrated without these fields will fail during target load in Migration Cockpit.
This function loops through each required field and logs every record where that field is blank, along with the account number and company code so you know exactly what to fix.
- Add the duplicate detection function below check_mandatory_fields, still above the if __name__ block:
def check_duplicates(target_df, key_fields):
# Find duplicate key combinations in the target data
findings = {
"rule": "Duplicate Detection",
"details": []
}
duplicates = target_df[target_df.duplicated(subset=key_fields, keep=False)]
if len(duplicates) > 0:
for _, row in duplicates.iterrows():
findings["details"].append({
"keys": {field: row[field] for field in key_fields},
"description": row.get("TXT20", "")
})
findings["status"] = "FAIL" if findings["details"] else "PASS"
return findings
What does this check catch?
Duplicate SAKNR + BUKRS combinations in the target data mean the same GL account was loaded more than once. In a real migration, this can cause posting errors and balance discrepancies.
The keep=False parameter flags every copy of a duplicate, not just the second instance. This way all occurrences show up in the findings.
You have added two new validation functions. Test them before moving on.
- In the if __name__ block at the bottom of your file, find this line:
print(check_record_counts(legacy_df, target_df))
- Add these two lines directly below it:
print(check_mandatory_fields(target_df, ["KTOKS", "WAERS", "FSTAG"]))
print(check_duplicates(target_df, ["SAKNR", "BUKRS"]))
- Save migration_validator.py and run it again:
python migration_validator.py
You should now see three dictionaries printed. The record count still shows PASS. The mandatory field check shows FAIL with details listing the accounts missing KTOKS, WAERS, and FSTAG. The duplicate check shows FAIL with account 210000 flagged.
Only seeing the record count result?
- Make sure you added the two new print lines inside the if __name__ block, indented to the same level as the existing print line.
- Check that both new functions (check_mandatory_fields and check_duplicates) are defined above the if __name__ block, not inside it.
- help me debug my migration validator output
The last check catches records that existed in the legacy system but never made it to the target.
- Add the missing records check below check_duplicates, above the if __name__ block:
def check_missing_records(legacy_df, target_df, key_fields):
# Identify legacy records that have no matching key in the target
findings = {
"rule": "Missing Records Check",
"details": []
}
legacy_keys = set(legacy_df[key_fields].apply(tuple, axis=1))
target_keys = set(target_df[key_fields].apply(tuple, axis=1))
missing = legacy_keys - target_keys
for key in missing:
key_dict = dict(zip(key_fields, key))
match = legacy_df
for field, value in key_dict.items():
match = match[match[field] == value]
desc = match["TXT20"].iloc[0] if len(match) > 0 else "Unknown"
findings["details"].append({
"keys": key_dict,
"description": desc
})
findings["status"] = "FAIL" if findings["details"] else "PASS"
return findings
What does this check catch?
This function builds a set of key combinations (SAKNR + BUKRS) from legacy data and another from target data, then finds keys that exist in legacy but not in target.
For each missing key, it looks up the original record description so the findings show meaningful names like "SGA Expenses" instead of just account numbers.
Run all validations and display the results
You now have four separate validation functions. You need an orchestrator to run them all in sequence and a display function to format the output.
- Add the run_all_validations function below check_missing_records, above the if __name__ block:
def run_all_validations(legacy_df, target_df):
# Execute all four validation rules and collect results
results = []
results.append(check_record_counts(legacy_df, target_df))
mandatory_fields = ["KTOKS", "WAERS", "FSTAG"]
results.append(check_mandatory_fields(target_df, mandatory_fields))
key_fields = ["SAKNR", "BUKRS"]
results.append(check_duplicates(target_df, key_fields))
results.append(check_missing_records(legacy_df, target_df, key_fields))
return results
What does this function do?
This is the orchestrator. It calls all four validation functions in sequence and collects their results into a single list. The mandatory fields (KTOKS, WAERS, FSTAG) and key fields (SAKNR, BUKRS) are defined here, keeping the validation rules configurable.
Verify that the orchestrator works before adding the formatted display.
- In the if __name__ block, replace the three individual print(check_...) lines with these two lines:
results = run_all_validations(legacy_df, target_df)
for result in results:
print(result)
- Save migration_validator.py and run it:
python migration_validator.py
You should see four dictionaries printed, one for each validation rule. All four checks run in sequence now through a single function call.
- Now add the display_raw_results function below run_all_validations, above the if __name__ block:
def display_raw_results(results):
# Print formatted output for each validation rule
print("=" * 60)
print("MIGRATION VALIDATION RESULTS (RAW)")
print("=" * 60)
for result in results:
print(f"\nRule: {result['rule']}")
print(f"Status: {result['status']}")
if "details" in result:
print(f"Issues found: {len(result['details'])}")
for detail in result["details"]:
print(f" - {detail}")
if "legacy_count" in result:
print(f" Legacy: {result['legacy_count']}, "
f"Target: {result['target_count']}, "
f"Diff: {result['difference']}")
print("\n" + "=" * 60)
What does this function do?
This function loops through the collected validation results and prints each rule's name, PASS/FAIL status, and detailed findings. For the record count rule, it also prints the legacy count, target count, and difference.
Finally, replace the temporary test block with the full main block that loads data, runs all validations, and displays results.
- Select the entire if __name__ block (everything from if __name__ == "__main__": to the end of the file) and replace it with this code:
if __name__ == "__main__":
# Load data and run the full validation pipeline
print("Loading migration data...")
legacy_df, target_df = load_data(
"data/legacy_gl_accounts.csv",
"data/target_gl_accounts.csv"
)
print(f"Legacy records loaded: {len(legacy_df)}")
print(f"Target records loaded: {len(target_df)}")
print("\nRunning validation rules...")
results = run_all_validations(legacy_df, target_df)
display_raw_results(results)
What does the main block do?
This is the entry point for your script. It loads both CSV files, prints how many records were found, runs all four validation rules through run_all_validations, and passes the results to display_raw_results for display.
- Save migration_validator.py.
Before you run this, think about what the output will look like. You know the tool catches real migration issues. Would you hand this output directly to a project manager?
- Run the full validation pipeline:
python migration_validator.py
You should see the full pipeline execute: data loads, validations run, and results print with PASS/FAIL status for each of the four rules along with detailed findings for every failure.
What should I see?
- Record Count Reconciliation: PASS (15 vs 15, difference 0).
- Mandatory Field Completeness: FAIL with findings for accounts missing KTOKS, WAERS, and FSTAG.
- Duplicate Detection: FAIL with account 210000 flagged.
- Missing Records Check: FAIL with account 600000 (SGA Expenses) identified as missing from the target.
The validator catches every issue you embedded in the sample data. But look at the output. The detailed findings are raw Python dictionaries. A project manager reading this would have to decode curly braces and nested key-value pairs to understand what went wrong.
Seeing a NameError or missing results?
- If you see a NameError for one of the validation functions, make sure all six functions (load_data, check_record_counts, check_mandatory_fields, check_duplicates, check_missing_records, run_all_validations) are defined above the if __name__ block.
- Check that you replaced the entire temporary test block with the final if __name__ code. No leftover print(check_record_counts(...)) lines should remain.
- help me debug my migration validator script
✔️ Awesome, I've got everything!
Make sure you have saved migration_validator.py.
ⓧ I'd like to double check the full code
Scroll up through your code and confirm that each of these sections is present, in this order from top to bottom:
- Imports: import pandas as pd, import anthropic, import json.
- load_data function: reads two CSVs with dtype=str and .fillna("").
- check_record_counts function: compares legacy vs target row counts.
- check_mandatory_fields function: finds blank mandatory fields in target data.
- check_duplicates function: detects duplicate key combinations.
- check_missing_records function: finds legacy records absent from target.
- run_all_validations function: calls all four checks and returns a list.
- display_raw_results function: prints each rule's name, status, and findings.
- if __name__ == "__main__" block: loads CSVs, runs validations, and calls display_raw_results.
Your validation engine catches every issue in the migration data. But the raw output reads like a data dump. Next, you will hand these findings to Claude and get back a migration report you could share with a project manager.
Add AI-Powered Analysis with Claude
In the previous step, you ran four validation rules and got results back. The problem is that those results are raw Python dictionaries.
Raw output tells you what failed, but not why it matters or what to do about it. In this step, you will connect your validator to Claude so it can transform those raw discrepancies into an analyst-grade migration report with root cause analysis and SAP-specific remediation steps.
In this step, get ready to:
- Write a function that sends validation findings to Claude with migration-consultant instructions.
- Run the full pipeline and review Claude's structured migration validation report.
Write the Claude analysis function
The Anthropic SDK is already imported in migration_validator.py from the previous step. You will add a new function that creates a client, builds a detailed prompt, and sends the validation results to Claude for analysis.
- In migration_validator.py, scroll down to the end of the display_raw_results function.
- After that function, add the first part of the analyze_with_claude function by copying the following code below the display_raw_results function:
def analyze_with_claude(results, api_key):
client = anthropic.Anthropic(api_key=api_key)
prompt = (
"You are an experienced SAP S/4HANA data migration consultant "
"analyzing validation results from a GL Account migration "
"(legacy ECC6 to S/4HANA).\n\n"
"Here are the validation results in JSON format:\n\n"
f"{json.dumps(results, indent=2)}\n\n"
"Please provide a structured migration validation report:\n\n"
"1. EXECUTIVE SUMMARY: Overall migration health status and "
"key risks\n"
"2. CRITICAL FINDINGS: Issues that would block go-live, with "
"root cause analysis\n"
"3. WARNINGS: Issues that need attention but are not blockers\n"
"4. RECOMMENDATIONS: Specific remediation steps for each "
"finding, referencing SAP best practices (e.g., mandatory "
"fields for S/4HANA, CVI requirements, Migration Cockpit "
"pre-load validation)\n"
"5. RECONCILIATION SUMMARY: A clear table showing legacy vs "
"target counts and pass/fail status for each check\n\n"
"Format the report so it could be shared directly with a "
"migration project manager."
)
What does this prompt do?
The prompt tells Claude to act as an experienced SAP S/4HANA migration consultant. It passes the raw validation results as JSON and asks for a five-section report: executive summary, critical findings with root cause analysis, warnings, remediation recommendations referencing SAP best practices, and a reconciliation summary table.
json.dumps(results, indent=2) converts the Python validation results into a formatted JSON string that Claude can parse. The last line of the prompt instructs Claude to format the report so it could be handed directly to a migration project manager.
- Directly below the prompt variable (still inside the analyze_with_claude function), add the API call that sends the prompt to Claude and returns the response:
message = client.messages.create(
model="claude-sonnet-4-5-20250929",
max_tokens=4096,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
What does this API call do?
client.messages.create sends the prompt to the claude-sonnet-4-5-20250929 model with a 4096-token response limit. That is more than enough for a detailed migration report.
message.content[0].text extracts the text body from Claude's response. The function returns this as a plain string that you can print to the terminal.
- Save migration_validator.py.
- Confirm there are no syntax errors by running the script in your terminal:
python migration_validator.py
You should see the same raw validation output from the previous step. The new function is defined but nothing calls it yet.
Seeing a syntax error?
- Make sure the analyze_with_claude function is placed after the display_raw_results function and before the if __name__ == "__main__": block.
- Check that the prompt string uses parentheses for multi-line continuation. Each line inside the parentheses should be a separate quoted string.
- Verify that the API call code (starting with message = client.messages.create) is indented one level inside the function, matching the prompt variable above it.
Why is my analyze_with_claude function causing a syntax error in migration_validator.py?
Connect Claude to the validation pipeline
The analysis function is ready. Now you need to call it from the __main__ block so the script sends validation findings to Claude after running all four checks.
- In migration_validator.py, scroll down to the if __name__ == "__main__": block.
- Find this line at the end of the block:
display_raw_results(results)
- Add the following code directly below that line, keeping the same indentation level:
print("\nSending findings to Claude for analysis...")
api_key = "your-api-key-here"
report = analyze_with_claude(results, api_key)
print("\n" + "=" * 60)
print("AI-POWERED MIGRATION VALIDATION REPORT")
print("=" * 60)
print(report)
What does this code do?
- api_key = "your-api-key-here" is a placeholder. You will replace it with your real Anthropic API key in the next step.
- analyze_with_claude(results, api_key) passes the raw validation results and your API key to the function you just wrote.
- The print statements frame Claude's response with a header so you can clearly see where the AI report starts in the terminal output.
- Find the line that says api_key = "your-api-key-here" in the __main__ block.
- Replace your-api-key-here with the actual API key you saved in your .env file earlier. Keep the surrounding quotes.
- Save migration_validator.py.
Keep your API key out of version control
If you ever push this project to a Git repository, make sure migration_validator.py does not contain your real API key. Swap it back to your-api-key-here before committing, or read the key from an environment variable instead.
Before you run the script, think about what you expect to see. The same four validation rules will run first. What do you think Claude will add on top of the raw output?
- Run the full pipeline by entering this command in your terminal:
python migration_validator.py
The script loads data, runs all four validation rules, prints the raw results, then sends the findings to Claude. After a few seconds, you will see a structured migration validation report printed below the header AI-POWERED MIGRATION VALIDATION REPORT.
Claude's report includes an executive summary, critical findings with root cause analysis, warnings, SAP-specific remediation recommendations, and a reconciliation summary table.
Why is this report more useful than the raw output?
Scroll up in your terminal and compare the raw output to Claude's report. The raw results are Python dictionaries listing each FAIL. Claude takes the same data and explains why each issue matters for go-live readiness.
For example, a missing FSTAG value in the raw output is just a field name and account number. In the report, Claude explains that FSTAG controls posting behavior in S/4HANA and recommends checking the Field Status Group configuration in the target system. That is the difference between a data dump and an actionable migration report you could hand to a project manager.
Seeing an authentication error?
- Double-check that you replaced your-api-key-here with your actual Anthropic API key. The key should start with sk-ant-.
- Make sure the key is wrapped in quotes on the api_key = line. The full line should look like api_key = "sk-ant-...".
- If your key was revoked or expired, go to https://platform.claude.com/settings/keys and generate a new one.
Why am I getting an authentication error when calling the Claude API from my Python script?
✔️ Awesome, I've got everything!
Great. Double check you have saved migration_validator.py with both the analyze_with_claude function and the updated __main__ block.
ⓧ I'd like to double check the full code
Your migration_validator.py is too long to display in a single block. Scroll through your file and confirm these sections appear in order:
- Imports at the top: import pandas as pd, import anthropic, import json.
- load_data function.
- check_record_counts function.
- check_mandatory_fields function.
- check_duplicates function.
- check_missing_records function.
- run_all_validations function.
- display_raw_results function.
- analyze_with_claude function (the new one you added in this step).
- if __name__ == "__main__": block, ending with print(report).
You now have a complete migration validation pipeline. Your script loads legacy and target data, runs four automated checks, and hands the raw findings to Claude for a structured, consultant-grade report. In the Secret Mission, you will extend this tool to handle a second data domain: Business Partners with CVI-specific validation rules.
Secret mission
Add Business Partner CVI Validation
Your validator handles GL Accounts, but real S/4HANA migrations convert customer and vendor master data into Business Partners through CVI (Customer Vendor Integration). CVI applies stricter validation rules than ECC ever did. Can you extend your tool to catch BP-specific issues before they block a go-live?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing cloud costs. Your Anthropic API key is pay-per-use and only incurs charges when you run the script.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Anthropic API key (pay-per-use, charges only when you make API calls).
- Local s4hana-validator project folder containing your Python virtual environment, scripts, and sample CSV data files.
Keep everything running
No action needed. Choose this if you plan to adapt this validator for your own migration data extracts.
- Your project files live in the s4hana-validator folder on your Desktop. Nothing is running or accumulating charges while idle.
- Your Anthropic API key will only be charged when you run migration_validator.py again. Each run costs less than $0.05.
- To reuse the tool with your own data, replace the CSVs in the data/ folder with your real legacy and target extracts.
Pause - I'll come back to this later
Nothing is running in the background. There are no servers, containers, or cloud resources to stop.
- Your virtual environment is only active in the terminal session where you ran venv\Scripts\activate. Closing that terminal deactivates it automatically.
- When you come back, open a terminal in the s4hana-validator folder and reactivate the virtual environment.
Delete - I don't want to use this again
Remove all project files and revoke your API access.
- Revoke your Anthropic API key by going to https://platform.claude.com/settings/keys.
- Find your API key in the list and click the delete button next to it.
- Delete the project folder. On Windows, open a terminal and run:
cd %USERPROFILE%\Desktop
rmdir /s /q s4hana-validator
On macOS or Linux, run:
cd ~/Desktop
rm -rf s4hana-validator
- Confirm the folder is gone by running ls (macOS/Linux) or dir (Windows) on your Desktop. You should no longer see s4hana-validator listed.
Nice Work!
Nice Work!
Nice work! You just built an automated S/4HANA migration data validator powered by Claude. What used to be hours of manual spreadsheet comparisons now runs in seconds and produces a report you could hand directly to a migration project manager.
You learned how to:
- Build a Python-based migration validation engine that automates four core S/4HANA data migration checks: record count reconciliation, mandatory field completeness, duplicate detection, and missing record identification.
- Integrate the Claude API to transform raw validation discrepancies into structured, consultant-grade migration reports with executive summaries, root cause analysis, and SAP-specific remediation recommendations.
- Create a reusable validation framework that works with any CSV data extracts and follows the real-world S/4HANA migration workflow: extract, validate, analyze, and remediate across multiple mock cycles.
- Secret Mission: Extended the validator to handle Business Partner data with CVI-specific validation rules for role assignment, mandatory fields, and duplicate detection.
Ready to quiz yourself?