GICT knowledge base
Python Automation: Tested Reports, API Reads and Email Previews
Published
Start Python automation with a task you can inspect before it changes anything: summarize a local report, read a public API or prepare an email preview. A useful script needs defined inputs, predictable outputs and a visible failure path, not just a timer that repeats it.
This guide provides three complete Python 3.12 scripts using only the standard library. The local report and email preview were tested with synthetic data. The HTTP example was tested with mocked success and failure responses; no live GitHub request or email delivery is claimed.
Prepare an isolated working directory
Confirm which interpreter runs your script: Windows installations may use py -3, while Linux or macOS commonly use python3. Check its version before running a command. Use a directory you control, ordinary user permissions and copies of sample data. These examples do not require third-party packages.
Do not start by scheduling a script with access to customer directories, a production mailbox or a personal messaging account. First verify a single run, its output and its failure behavior.
1. Preview a CSV report without overwriting the source
The input must have the exact header job,status. Each record needs a non-empty job identifier and one of three exact statuses: ok, failed or skipped. This counts records, not unique jobs. A header-only file produces three zero counts; malformed records fail rather than being silently dropped.
Example input named jobs.csv:
job,status
backup,ok
cleanup,ok
sync,failed
The following script, status_report.py, reads UTF-8 input, including an optional byte-order mark. It prints a preview unless an output path is explicitly supplied. It validates the full input before opening the output.
import argparse
import csv
import io
from pathlib import Path
def build_report(source):
counts = {"ok": 0, "failed": 0, "skipped": 0}
with Path(source).open(encoding="utf-8-sig", newline="") as stream:
rows = csv.reader(stream, strict=True)
if next(rows, None) != ["job", "status"]:
raise ValueError("Expected the header job,status")
for row in rows:
if len(row) != 2 or not row[0].strip() or row[1] not in counts:
raise ValueError(f"Invalid record ending on line {rows.line_num}")
counts[row[1]] += 1
output = io.StringIO(newline="")
writer = csv.writer(output)
writer.writerow(["status", "count"])
writer.writerows(counts.items())
return output.getvalue()
def main():
parser = argparse.ArgumentParser(description="Preview a job-status summary")
parser.add_argument("source")
parser.add_argument("--output", help="Create a new CSV; never overwrite")
args = parser.parse_args()
try:
report = build_report(args.source)
if args.output:
with Path(args.output).open("x", encoding="utf-8", newline="") as stream:
stream.write(report)
else:
print(report, end="")
except (OSError, ValueError, csv.Error) as error:
parser.exit(2, f"Report failed: {error}\n")
if __name__ == "__main__":
main()
Example commands for a Linux or macOS environment where python3 is the intended interpreter:
python3 status_report.py jobs.csv
python3 status_report.py jobs.csv --output summary-new.csv
The first command prints counts of 2, 1 and 0. The second creates a new file and refuses to overwrite any existing destination, including the source. Exclusive creation is not a complete atomic-write or recovery strategy: a disk failure during writing can leave a partial new file. Inspect failures before retrying. The output contains fixed status labels and integer counts, not arbitrary spreadsheet formulas from input fields.
2. Read a documented API with bounded input
A documented API is usually a more stable starting point than guessing an HTML selector. This example reads the public python/cpython repository endpoint on GitHub and returns three fields. It uses HTTPS, a timeout, a response-size limit and explicit schema checks. It does not require or transmit a token.
import json
import sys
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
def repository_summary():
request = Request(
"https://api.github.com/repos/python/cpython",
headers={"Accept": "application/vnd.github+json",
"User-Agent": "InterServer-Python-Tutorial"},
)
with urlopen(request, timeout=10) as response:
raw = response.read(128_001)
if len(raw) > 128_000:
raise ValueError("Response exceeded the tutorial's size limit")
data = json.loads(raw)
if not isinstance(data, dict):
raise ValueError("Expected a repository object")
for key in ["full_name", "default_branch"]:
if not isinstance(data.get(key), str) or not data[key]:
raise ValueError(f"Missing or invalid {key}")
if type(data.get("archived")) is not bool:
raise ValueError("Missing or invalid archived flag")
return {key: data[key] for key in ["full_name", "default_branch", "archived"]}
if __name__ == "__main__":
try:
print(json.dumps(repository_summary(), indent=2))
except HTTPError as error:
sys.exit(f"GitHub request failed with HTTP {error.code}; check access and rate limits")
except (URLError, TimeoutError, OSError, ValueError):
sys.exit("Repository request failed; check the connection and response format")
Running repository_summary.py makes one external GET request. HTTP errors, connection failures and malformed responses cause a nonzero exit. The timeout is not a complete end-to-end job deadline. Respect the provider’s rate limits, and do not put a failing request in an unlimited retry loop. For a private API, use its documented authentication flow and a narrowly scoped secret outside source code.
If an API is unavailable and you need HTML extraction, verify permission and applicable access rules, use timeouts, check the response and handle a missing selector. A placeholder such as example.com/weather is not a working weather data source. Do not bypass access controls or assume a site’s HTML will stay unchanged.
3. Compose an email attachment without sending it
Email composition and email delivery are separate operations. This script builds a message with a synthetic CSV attachment and prints its MIME representation. The example.com addresses are reserved examples. There is deliberately no SMTP connection or send call.
import csv
import io
from email.message import EmailMessage
from email.policy import SMTP
def compose_preview(counts):
statuses = ["ok", "failed", "skipped"]
if set(counts) != set(statuses):
raise ValueError("Provide ok, failed and skipped counts")
if any(type(counts[key]) is not int or counts[key] < 0 for key in statuses):
raise ValueError("Counts must be non-negative integers")
report = io.StringIO(newline="")
writer = csv.writer(report)
writer.writerow(["status", "count"])
writer.writerows((key, counts[key]) for key in statuses)
message = EmailMessage(policy=SMTP)
message["From"] = "automation@example.com"
message["To"] = "operator@example.com"
message["Subject"] = "Job status preview - not sent"
message.set_content("Review the attached synthetic status summary before sending.")
message.add_attachment(report.getvalue().encode("utf-8"),
maintype="text", subtype="csv", filename="status-summary.csv")
return message
if __name__ == "__main__":
preview = compose_preview({"ok": 2, "failed": 1, "skipped": 0})
print(preview.as_string())
Before adding delivery, select your mail provider’s documented SMTP or API setup, protect credentials, validate the recipient list and use a test mailbox. For SMTP, use certificate-verified TLS, a timeout and the provider’s supported authentication mechanism; a normal account password is not universally accepted. Track deliveries so a retry does not repeatedly send the same report. Never include credentials or private attachments in shared debug output.
What changes for Excel, messaging and file operations?
- Excel workbooks: CSV is plain tabular data, not a workbook with formulas, formatting, charts or macros. Do not assume a read-and-write operation through a data-frame library preserves those features. Test workbook-specific tools against a copy before using them on originals.
- Contact cleanup: An address containing
@is not proof it is valid or deliverable. Changing every name to title case can also damage intended spelling. Define a reviewable cleaning rule rather than silently rewriting personal data. - Messaging and social posts: Use the platform’s supported API and a test destination. UI automation can click the wrong window or send to the wrong person. Do not treat an unauthenticated snippet or an obsolete posting method as a working integration.
- Moving or renaming files: Produce a proposed change list first, check collisions and keep backups. The mini-project guide includes a non-destructive file plan rather than automatic moves.
Schedule only after checking failure and repeat behavior
A scheduler starts a process; it does not fix missing permissions, a wrong working directory or an expired credential. Configure the absolute interpreter and script paths, explicit input/output paths, the intended account and a known time zone. Capture exit status and restricted logs. Use a run lock if overlapping jobs would conflict, and define how repeat runs avoid duplicate side effects.
Do not catch every exception and print a success message. Either recover deliberately from a specific failure or report a nonzero exit. Logging configuration alone does not record exceptions; code must emit a log record. See reading and handling Python tracebacks.
Verification: Tested on Python 3.12.14 using disposable local fixtures: report counts, empty input, malformed records, quoted CSV, no-overwrite behavior, source preservation, CLI failure status, HTTP success/error/timeout/schema/size handling, and MIME attachment round-tripping. Network responses were mocked. No real mailbox, customer file, workbook, social account or scheduled job was exercised.
References: Python CSV handling, GitHub repository API, Python email examples and SMTP client documentation.
Related guides
How to Deploy a FastAPI App with Gunicorn and Nginx
Deploy FastAPI behind Nginx with Gunicorn and systemd. Separate application packages, check HTTPS and verify worker replacement and application health.
PythonLearn How to Modify Python Dictionary Values Easily
Learn how to modify Python dictionary values effectively with various methods, including direct assignment, update(), and comprehensions, along with common mistakes to...
PythonPython Set Copy Tutorial: Avoid Common Mistakes and Data Errors
Learn how to copy sets in Python safely and efficiently while avoiding common mistakes. Explore methods like copy(), set(), and deepcopy().
PythonDifferent Methods to Loop Through Sets in Python (Beginner-Friendly Guide)
Learn diverse methods to loop through sets in Python, including practical examples and use cases, ideal for beginners looking to master set operations.
