By Pindi Sahota · Last updated: 2026-06-07
This page contains affiliate links. If you purchase through them, I may earn a commission at no extra cost to you.
Claude for Python Scripting — Developer Guide (2026)
Last updated: 2026-06-07
Claude Python scripting is one of the most practical use cases for AI-assisted development. Python's combination of readable syntax, vast ecosystem, and heavy use in automation makes it ideal for AI generation — and Claude is exceptionally good at it. Give Claude a plain-English description of what you need, and it returns a complete, runnable script with error handling, type hints, logging, and argument parsing — the parts that are correct but tedious to write by hand. This guide covers how to get the best Python output from Claude, with three complete scripts you can run today, and how to chain Python scripts with tools like Make.com for full automation pipelines.
How Claude Helps with Python Scripting
Claude does not just autocomplete Python — it understands intent. When you say "write a script to process customer CSV exports and load them to Postgres, skipping duplicates", Claude figures out:
- Which libraries to use (pandas + SQLAlchemy, or psycopg2 directly)
- How to handle CSV encoding edge cases
- What the upsert SQL should look like
- How to add
--dry-runmode and--verboseflags - How to log progress and failures
The result is a script you would write yourself, but in minutes instead of an hour. Here is how to get consistently good output.
How to Get Great Python Scripts from Claude — Step by Step
Step 1: Be Specific About Inputs and Outputs
The most common mistake is an under-specified prompt. Instead of:
` Write a script to process CSV files `
Say:
` Write a Python script that:
- Reads all .csv files from an input/ directory
- Each CSV has columns: customer_id, email, amount, date (YYYY-MM-DD)
- Validates that amount is a positive number and email is valid format
- Writes valid rows to output/clean.csv and invalid rows to output/errors.csv with an added 'error_reason' column
- Logs progress to stdout and a logfile
- Accepts --input-dir and --output-dir as CLI arguments
`
Step 2: Specify Your Python Version and Key Libraries
Claude defaults to Python 3.10+ idioms, but if you need compatibility with an older version or have specific library preferences, say so:
` Use Python 3.11. Use httpx instead of requests for async support. Use pydantic v2 for validation. `
Step 3: Ask for Error Handling and Logging Explicitly (or Tell Claude to Add It)
Claude adds error handling when you ask for "production-ready" or "robust" scripts. A quick addition to your prompt:
` Add proper error handling, retry logic for network calls, and structured logging using the standard logging module. `
Step 4: Iterate with Claude Code for File-Based Projects
For multi-file Python projects, Claude Code (the CLI) is more powerful than the browser interface. It reads your existing code, maintains consistency with your patterns, and can run the script to verify it works:
`bash cd ~/projects/data-pipeline claude
Refactor the csv_processor.py script to use a class-based
approach so we can subclass it for JSON and XML inputs too. Run the tests after refactoring. `
Complete Python Script Examples from Claude
Script 1: REST API Polling and Alert Tool
`python
#!/usr/bin/env python3 """ Poll a REST API endpoint at regular intervals and send an alert when a condition is met. Uses httpx for async HTTP and sends alerts via a webhook URL (compatible with Slack, Discord, Make.com).
Usage: python api_monitor.py --url https://api.example.com/status \ --webhook https://hooks.slack.com/... \ --interval 60 """
import argparse import asyncio import logging import sys from datetime import datetime
import httpx
logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", handlers=[ logging.StreamHandler(sys.stdout), logging.FileHandler("monitor.log"), ], ) log = logging.getLogger(name)
async def checkendpoint(client: httpx.AsyncClient, url: str) -> dict: """Fetch the endpoint and return parsed JSON.""" response = await client.get(url, timeout=10.0) response.raisefor_status() return response.json()
def isalertcondition(data: dict) -> tuple[bool, str]: """Return (shouldalert, reason). Customise this logic.""" status = data.get("status", "") if status not in ("ok", "healthy"): return True, f"Status is '{status}'" errorrate = data.get("errorratepct", 0) if errorrate > 5: return True, f"Error rate {errorrate:.1f}% exceeds 5%" return False, ""
async def sendalert(client: httpx.AsyncClient, webhookurl: str, reason: str) -> None: """POST an alert message to the webhook URL.""" payload = { "text": f":warning: API Monitor Alert — {reason} at {datetime.utcnow().isoformat()}Z" } try: resp = await client.post(webhookurl, json=payload, timeout=10.0) resp.raisefor_status() log.info("Alert sent: %s", reason) except httpx.HTTPError as e: log.error("Failed to send alert: %s", e)
async def runmonitor(url: str, webhookurl: str, interval: int) -> None: """Main monitoring loop.""" log.info("Starting monitor for %s, polling every %ds", url, interval) last_alerted = False
async with httpx.AsyncClient() as client: while True: try: data = await checkendpoint(client, url) shouldalert, reason = isalertcondition(data)
if shouldalert and not lastalerted: await sendalert(client, webhookurl, reason) lastalerted = True elif not shouldalert and lastalerted: log.info("Condition resolved — no longer alerting") lastalerted = False else: log.debug("Status OK")
except httpx.HTTPError as e: log.error("Request failed: %s", e) if not lastalerted: await sendalert(client, webhookurl, f"Request failed: {e}") lastalerted = True
await asyncio.sleep(interval)
def main() -> None: parser = argparse.ArgumentParser(description="API endpoint monitor") parser.addargument("--url", required=True, help="Endpoint URL to monitor") parser.addargument("--webhook", required=True, help="Webhook URL for alerts") parser.addargument("--interval", type=int, default=60, help="Poll interval in seconds") args = parser.parseargs()
try: asyncio.run(run_monitor(args.url, args.webhook, args.interval)) except KeyboardInterrupt: log.info("Monitor stopped")
if name == "main": main() `
Script 2: CSV-to-PostgreSQL ETL Pipeline
`python
#!/usr/bin/env python3 """ Read CSV files from an input directory, validate rows, and upsert valid rows into a PostgreSQL table. Invalid rows are written to an error CSV with a reason column.
Usage: python csvtopg.py --input-dir ./data --db-url postgresql://user:pass@localhost/mydb """
import argparse import csv import logging import re import sys from pathlib import Path
import psycopg2 from psycopg2.extras import execute_values
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") log = logging.getLogger(name)
EMAIL_RE = re.compile(r"^[^@]+@[^@]+\.[^@]+$")
UPSERTSQL = """ INSERT INTO customers (customerid, email, amount, saledate) VALUES %s ON CONFLICT (customerid) DO UPDATE SET email = EXCLUDED.email, amount = EXCLUDED.amount, saledate = EXCLUDED.saledate """
def validaterow(row: dict) -> tuple[bool, str]: if not row.get("customerid", "").strip(): return False, "Missing customerid" if not EMAILRE.match(row.get("email", "")): return False, f"Invalid email: {row.get('email')}" try: amount = float(row["amount"]) if amount <= 0: raise ValueError except (KeyError, ValueError): return False, f"Invalid amount: {row.get('amount')}" return True, ""
def processfile(filepath: Path, conn) -> tuple[int, int]: """Returns (inserted, rejected) counts.""" validrows = [] error_rows = []
with filepath.open(newline="", encoding="utf-8-sig") as f: reader = csv.DictReader(f) for row in reader: ok, reason = validaterow(row) if ok: validrows.append( (row["customerid"].strip(), row["email"].strip(), float(row["amount"]), row["date"].strip()) ) else: errorrows.append({**row, "error_reason": reason})
if validrows: with conn.cursor() as cur: executevalues(cur, UPSERTSQL, validrows) conn.commit()
if errorrows: errorpath = filepath.parent / "errors" / filepath.name errorpath.parent.mkdir(existok=True) with errorpath.open("w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=list(errorrows[0].keys())) writer.writeheader() writer.writerows(error_rows)
log.info("%s: %d inserted, %d rejected", filepath.name, len(validrows), len(errorrows)) return len(validrows), len(errorrows)
def main() -> None: parser = argparse.ArgumentParser(description="CSV to PostgreSQL ETL") parser.addargument("--input-dir", required=True, type=Path) parser.addargument("--db-url", required=True) args = parser.parse_args()
csvfiles = list(args.inputdir.glob("*.csv")) if not csvfiles: log.warning("No CSV files found in %s", args.inputdir) sys.exit(0)
conn = psycopg2.connect(args.dburl) totalinserted = total_rejected = 0
try: for filepath in csvfiles: inserted, rejected = processfile(filepath, conn) totalinserted += inserted totalrejected += rejected finally: conn.close()
log.info("Done. Total inserted: %d, rejected: %d", totalinserted, totalrejected)
if name == "main": main() `
Script 3: Batch File Renamer with Preview Mode
`python
#!/usr/bin/env python3 """ Rename files in a directory according to a pattern. Supports preview (dry-run) mode, undo via a log file, and regex transformations.
Usage: # Preview python renamer.py --dir ./photos --pattern "{date}{name}" --dry-run # Apply python renamer.py --dir ./photos --pattern "{date}{name}" # Undo last run python renamer.py --undo """
import argparse import json import re import sys from datetime import datetime from pathlib import Path
UNDOLOG = Path(".renamerundo.json")
def buildnewname(filepath: Path, pattern: str) -> str: """Substitute pattern variables with file metadata.""" mtime = datetime.fromtimestamp(filepath.stat().stmtime) replacements = { "{name}": filepath.stem, "{ext}": filepath.suffix.lstrip("."), "{date}": mtime.strftime("%Y-%m-%d"), "{datetime}": mtime.strftime("%Y%m%d%H%M%S"), } result = pattern for key, value in replacements.items(): result = result.replace(key, value) # Append original extension if pattern doesn't include {ext} if "{ext}" not in pattern and filepath.suffix: result += filepath.suffix return result
def renamefiles(directory: Path, pattern: str, dryrun: bool) -> None: files = [f for f in directory.iterdir() if f.is_file() and not f.name.startswith(".")] if not files: print("No files found.") return
undo_map = {} conflicts = set()
renames = [(f, f.parent / buildnewname(f, pattern)) for f in files]
# Check for conflicts newnames = [new for , new in renames] seen = set() for new in new_names: if new in seen: conflicts.add(new) seen.add(new)
for original, newpath in renames: if original == newpath: continue if newpath in conflicts: print(f" SKIP (conflict): {original.name} -> {newpath.name}") continue print(f" {'[DRY RUN] ' if dryrun else ''}Rename: {original.name} -> {newpath.name}") if not dryrun: original.rename(newpath) undomap[str(newpath)] = str(original)
if not dryrun and undomap: UNDOLOG.writetext(json.dumps(undomap, indent=2)) print(f"\nUndo log saved to {UNDOLOG}")
def undorenames() -> None: if not UNDOLOG.exists(): print("No undo log found.") sys.exit(1) undomap = json.loads(UNDOLOG.readtext()) for current, original in undomap.items(): currentpath, originalpath = Path(current), Path(original) if currentpath.exists(): currentpath.rename(originalpath) print(f"Restored: {currentpath.name} -> {originalpath.name}") else: print(f"File not found, skipping: {currentpath.name}") UNDO_LOG.unlink() print("Undo complete.")
def main() -> None: parser = argparse.ArgumentParser(description="Batch file renamer") parser.addargument("--dir", type=Path, help="Directory containing files to rename") parser.addargument("--pattern", help="Name pattern. Variables: {name} {ext} {date} {datetime}") parser.addargument("--dry-run", action="storetrue", help="Preview without renaming") parser.addargument("--undo", action="storetrue", help="Undo the last rename operation") args = parser.parse_args()
if args.undo: undorenames() elif args.dir and args.pattern: renamefiles(args.dir, args.pattern, args.dryrun) else: parser.printhelp() sys.exit(1)
if name == "main": main() `
Integrating Claude Python Output with Make.com
Claude can write Python scripts that serve as the processing core of a Make.com automation. A common pattern: Make.com handles the trigger and data routing, and a Python script (deployed as an AWS Lambda or on a VPS) handles the heavy lifting.
Example workflow:
- Make.com trigger — new row in Google Sheets
- Make.com HTTP module — POST the row data to your Python webhook endpoint
- Python script — validates, transforms, and loads data to Postgres
- Make.com — receives the response, updates the Sheet status
Ask Claude to generate the Python Flask endpoint that Make.com calls:
` Write a Flask endpoint at POST /process-lead that receives JSON with fields (name, email, company, source). Validate the data with pydantic, upsert to the leads table in Postgres, and return {success: true, lead_id: ...} or {success: false, error: ...}. The endpoint should use HTTP Basic Auth with credentials from environment variables. `
Claude generates a complete, deployable Flask handler with pydantic validation, database upsert, and authentication — ready to plug into your Make.com scenario.
Claude Code vs Claude.ai for Python Projects
| Use case | Use Claude Code | Use Claude.ai |
|---|---|---|
| Multi-file Python project | Yes — reads all files | No |
| Single standalone script | Either works | Quick and easy |
| Refactoring existing code | Claude Code | Paste and describe |
| Running and testing output | Claude Code | No |
| Quick data manipulation | Either | Faster for one-offs |
| Package a CLI tool | Claude Code | Possible but awkward |