Lesson 12 of 16 · Level 3: Advanced
Python APIs and Automation: requests, Files and Reports
- ⏱ 15 min read
- ❓ 4 review questions
- ✍️ Exercise with solution
In this lesson
- What an API is
- Your first request with requests
- A robust function
- Parameters and API keys
- Automating files with pathlib
- Generating Excel reports
- Making it run on its own
- Real case: an exchange-rate alert for an importer
- Common mistakes with APIs and automation
- Final project: your expenses in two currencies, every morning
- Summary
- Call web APIs with
requestsand work with their JSON responses. - Keep API keys safe with environment variables.
- Automate file tasks: organize folders and generate Excel reports.
- Schedule your scripts so they run on their own.
- Read network errors (404, 401, timeouts) and keep a log of what each script does.
This lesson closes the Advanced level. So far your programs have worked with data you gave them. Now you'll connect them to the world: live exchange rates, web services, folders that organize themselves and reports that build themselves every morning.
timeout is enough to make sure your script never hangs foreverWhat an API is
A web API is a "door" a service opens so other programs can request data or send instructions. Instead of a web page for people, it returns data for programs, almost always as JSON (the same format as lesson 8). Examples: exchange rates, weather, payments (Stripe), maps, social networks and AI models.
The conversation always looks similar:
- Your program makes a request to a URL:
GETto ask for data,POSTto send it. - The server answers with a status code (200 = OK, 404 = not found, 401 = unauthorized, 429 = too many requests, 500 = server error) plus the data.
- 2xx · success 200Read the data with
.json(). - 4xx · problem with your request 401 · 404 · 429Check the URL or the key, or slow down your requests.
- 5xx · server error 500Not your fault: try again later.
requests.get() repeats this cycle. raise_for_status() turns 4xx and 5xx responses into exceptions.Your first request with requests
pip install requests
import requests
url = "https://open.er-api.com/v6/latest/USD" # free public exchange-rate API
response = requests.get(url, timeout=10)
response.raise_for_status() # raises an exception if the status isn't 2xx
data = response.json() # turns the JSON into a dictionary
print(data["base_code"]) # USD
print(data["rates"]["EUR"]) # euros per dollar
print(data["rates"]["MXN"]) # Mexican pesos per dollar
timeout=10: if the server doesn't answer within 10 seconds, you don't wait forever. Always set it.raise_for_status(): turns HTTP errors into exceptions you can catch..json(): gives you Python dictionaries and lists, which you already know how to loop through (lesson 6).
Rates change daily, so your numbers will differ. Before using any API, read its documentation: the URL, parameters, response format, request limits and terms of use.
1. An API answers with status code 429. What does it mean?
429 means Too Many Requests. Wait, or lower how often you call. A 404 would mean "not found."
A robust function
import requests
def exchange_rate(base="USD", target="EUR"):
"""Return how many units of 'target' 1 unit of 'base' buys, or None on failure."""
try:
r = requests.get(f"https://open.er-api.com/v6/latest/{base}", timeout=10)
r.raise_for_status()
return r.json()["rates"][target]
except requests.RequestException as e:
print(f"Connection error: {e}")
except KeyError:
print(f"Currency not found: {target}")
return None
Internet requests fail more often than you'd think: no connection, a server that's down, an API that changed. A professional program plans for it with try/except (lesson 8). requests.RequestException covers timeouts, connection problems and the HTTP errors raised by raise_for_status().
Parameters and API keys
Many APIs take parameters and a key (API key) that identifies your account. Here's what a call to a weather service usually looks like:
import os
import requests
key = os.environ.get("WEATHER_API_KEY") # read from the environment, never written in code
if not key:
raise SystemExit("Missing the WEATHER_API_KEY environment variable")
params = {"q": "Denver", "units": "imperial", "appid": key}
r = requests.get("https://api.example-weather.com/weather", params=params, timeout=10)
If you push a file with a key to GitHub, anyone can use it and run up your bill. Store keys in environment variables (set WEATHER_API_KEY=… in Windows Command Prompt, $env:WEATHER_API_KEY="…" in PowerShell, export WEATHER_API_KEY=… on macOS and Linux) or in a .env file listed in your .gitignore (with the python-dotenv library).
2. What's the right way to use an API key in your script?
That way the key never lives in your code and can't be shared by accident when you publish the project.
Automating files with pathlib
One of the most satisfying automations: sorting your Downloads folder by file type.
from pathlib import Path
import shutil
FOLDER = Path.home() / "Downloads"
TYPES = {
"Documents": {".pdf", ".docx", ".txt"},
"Spreadsheets": {".xlsx", ".csv"},
"Images": {".jpg", ".jpeg", ".png"},
}
for file in FOLDER.iterdir():
if not file.is_file():
continue
for target, extensions in TYPES.items():
if file.suffix.lower() in extensions:
target_folder = FOLDER / target
target_folder.mkdir(exist_ok=True)
shutil.move(str(file), target_folder / file.name)
print(f"Moved: {file.name} → {target}")
break
Path.home() / "Downloads"builds paths that work on Windows, macOS and Linux.iterdir()walks the folder's contents;suffixis the extension (lowercased, so ".JPG" counts as an image).mkdir(exist_ok=True)creates the folder only if it doesn't exist yet. Files with other extensions, like.exe, stay where they are.
Before moving or deleting real files, replace shutil.move with a print and check what the script would do. Then test it on a copy of the folder.
Generating Excel reports
pandas (lesson 11) creates Excel files with basic formatting; openpyxl gives you control over every cell, including real formulas:
from openpyxl import Workbook
from openpyxl.styles import Font
wb = Workbook()
sheet = wb.active
sheet.title = "Summary"
sheet.append(["Category", "Amount"])
for category, amount in [("Food", 739.41), ("Housing", 404.31), ("Entertainment", 311.98)]:
sheet.append([category, amount])
for cell in sheet[1]:
cell.font = Font(bold=True)
sheet["A5"] = "Total"
sheet["B5"] = "=SUM(B2:B4)"
wb.save("summary.xlsx")
Making it run on its own
Windows
Task Scheduler → Create Basic Task → "Start a program," pointing to the python.exe inside your virtual environment, with your script as the argument.
macOS and Linux
cron. Monday to Friday at 8:00 a.m., in crontab -e:0 8 * * 1-5 /path/to/project/.venv/bin/python /path/to/project/report.py
In the cloud
GitHub Actions or serverless functions run scripts on a schedule without leaving your computer on.
For automated scripts, record what happens in a log file with the logging module instead of print: that way you'll know what happened even when nobody was watching.
Real case: an exchange-rate alert for an importer
A small furniture importer in Miami pays its Italian suppliers in euros and wants to know each morning whether to pay today or wait. On a €48,000 invoice, the difference between $1 buying €0.842 and €0.871 is about $1,898. This script keeps a CSV history, writes a log and returns a clear message. The logic is kept separate from the internet call so you can test it with made-up values:
import csv
import logging
from datetime import date
from pathlib import Path
FOLDER = Path(__file__).parent # works even when Task Scheduler or cron runs it
HISTORY = FOLDER / "usd_eur_history.csv"
logging.basicConfig(filename=FOLDER / "alert.log", level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s")
THRESHOLD = 0.85 # warn if $1 buys less than €0.85
def record(rate):
is_new = not HISTORY.exists()
with open(HISTORY, "a", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
if is_new:
writer.writerow(["date", "usd_eur"])
writer.writerow([date.today().isoformat(), rate])
def check(rate, threshold=THRESHOLD):
record(rate)
if rate < threshold:
logging.warning("Weak dollar: %.3f < %.2f", rate, threshold)
return f"ALERT: $1 buys only €{rate:.3f}. Consider waiting to pay the supplier."
logging.info("Dollar at %.3f", rate)
return f"OK: $1 buys €{rate:.3f}. Good time to pay."
# Test the logic with made-up values (no internet needed)
print(check(0.842))
print(check(0.871))
ALERT: $1 buys only €0.842. Consider waiting to pay the supplier.
OK: $1 buys €0.871. Good time to pay.
In production, you'd replace the two test lines at the bottom with rate = exchange_rate("USD", "EUR") (the robust function from above) and, if it isn't None, print(check(rate)). The alert.log file looks like this:
2026-11-15 00:19:08,701 WARNING Weak dollar: 0.842 < 0.85
2026-11-15 00:19:08,702 INFO Dollar at 0.871
The line FOLDER = Path(__file__).parent is the key to automation: __file__ is your script's own path, so the history and the log are saved next to it even when Task Scheduler or cron runs it from another folder.
Common mistakes with APIs and automation
When the URL is mistyped or the currency in the path doesn't exist, raise_for_status() raises an error like this:
requests.exceptions.HTTPError: 404 Client Error: Not Found for url: https://open.er-api.com/v6/latest/XYZ
Read it in three parts: the code (404), its meaning (Not Found) and the exact URL that was requested. Paste that URL into your browser: the problem is usually obvious.
Calling requests.get() without a timeout.
If the server never answers, your automated script hangs. Always set timeout=10.
Calling .json() without checking the status code.
Call raise_for_status() first: an error page doesn't contain the data you expect.
Using relative paths in a scheduled script.
The scheduler runs it from another folder and you get FileNotFoundError. Build paths with Path(__file__).parent.
Scheduling the task with the system Python.
Use the python inside your .venv; otherwise the log will show ModuleNotFoundError.
3. Your script is about to move hundreds of files. What do you do before running it for real?
A script's actions on files can't be undone with one click. Testing first prevents disasters.
4. Your report works when you run it by hand, but when cron runs it, it fails with FileNotFoundError: [Errno 2] No such file or directory: 'expenses.csv'. What do you change?
cron runs the script from a different working folder (usually your home folder), so the relative path "expenses.csv" points somewhere else. With Path(__file__).parent, the path is always relative to the script.
Final project: your expenses in two currencies, every morning
This script brings the whole course together: it reads lesson 8's expense CSV, gets an exchange rate from an API, builds the summary with pandas and saves an Excel file. Imagine you split costs with a business partner in Toronto who thinks in Canadian dollars; schedule it and the report is waiting every morning.
import logging
from datetime import date
from pathlib import Path
import pandas as pd
import requests
FOLDER = Path(__file__).parent
logging.basicConfig(filename=FOLDER / "report.log", level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s")
SECOND_CURRENCY = "CAD" # change to EUR, MXN, GBP...
FILE = FOLDER / "expenses.csv"
def get_exchange_rate(currency):
r = requests.get("https://open.er-api.com/v6/latest/USD", timeout=10)
r.raise_for_status()
return r.json()["rates"][currency] # units of currency per 1 USD
def build_report():
if not FILE.exists():
logging.warning("%s not found", FILE)
return
expenses = pd.read_csv(FILE)
rate = get_exchange_rate(SECOND_CURRENCY)
summary = expenses.groupby("category", as_index=False)["amount"].sum()
summary[f"amount_{SECOND_CURRENCY.lower()}"] = (summary["amount"] * rate).round(2)
summary = summary.sort_values("amount", ascending=False)
output = FOLDER / f"report_{date.today().isoformat()}.xlsx"
summary.to_excel(output, index=False)
logging.info("Report %s created (1 USD = %s %s)", output.name, rate, SECOND_CURRENCY)
print(summary.to_string(index=False))
if __name__ == "__main__":
try:
build_report()
except requests.RequestException as e:
logging.error("Could not get the exchange rate: %s", e)
With the practice expenses.csv and a sample rate of 1 USD = 1.384 CAD, it prints the table below (your Canadian-dollar column will reflect that day's real rate):
category amount amount_cad
food 739.41 1023.34
housing 404.31 559.57
entertainment 311.98 431.78
transportation 291.38 403.27
Practice exercise: finish the project
- Run the project with your own
expenses.csvand check thereport.logfile. - Add a second sheet to the Excel file with spending by month (hint:
pd.ExcelWriter, like in lesson 11). - Schedule it to run every Monday at 8:00 a.m.
- Reorganize it as a package with tests (lesson 10) and publish it on GitHub as part of your portfolio, with no keys or personal data.
See solution
# Inside build_report(), replacing the to_excel line:
expenses["month"] = pd.to_datetime(expenses["date"]).dt.to_period("M").astype(str)
by_month = expenses.groupby("month", as_index=False)["amount"].sum()
with pd.ExcelWriter(output) as writer:
summary.to_excel(writer, sheet_name="By category", index=False)
by_month.to_excel(writer, sheet_name="By month", index=False)
# crontab -e (Mondays at 8:00 a.m.):
# 0 8 * * 1 /path/to/project/.venv/bin/python /path/to/project/report.py
With the practice file, the "By month" sheet shows $795.05 for September and $952.03 for October. And if the API doesn't respond, the requests.RequestException is caught in the main block and written to report.log, so the script ends cleanly instead of hanging.
Summary
requests.get(url, timeout=10),raise_for_status()and.json()are the core of every API call.- Keep API keys in environment variables or a git-ignored
.envfile, never in code. pathlibandshutilautomate file tasks; always do a dry run first.- Schedule scripts with Task Scheduler or cron, use
Path(__file__).parentfor paths andlogginginstead ofprint. - You've now covered the Beginner, Intermediate and Advanced levels: from variables to classes, pandas and APIs.
You've completed the Advanced level! You can now build programs that read data, analyze it, talk to web services and run on their own. Next comes Level 4: Expert, with skills that data and developer job postings ask for constantly.
Next lesson: Python and SQL: Databases with SQLite from Scratch. You'll learn SQL and how to store and query data from Python with SQLite.
Create your account in 30 seconds, mark your lessons and download your certificate at the end.