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
  1. What an API is
  2. Your first request with requests
  3. A robust function
  4. Parameters and API keys
  5. Automating files with pathlib
  6. Generating Excel reports
  7. Making it run on its own
  8. Real case: an exchange-rate alert for an importer
  9. Common mistakes with APIs and automation
  10. Final project: your expenses in two currencies, every morning
  11. Summary
What you'll learn
  • Call web APIs with requests and 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.

200is the status code for "all good"; anything else deserves your attention
10 sof timeout is enough to make sure your script never hangs forever
0API keys written in your code: they always go in environment variables

What 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:

  1. Your program makes a request to a URL: GET to ask for data, POST to send it.
  2. The server answers with a status code (200 = OK, 404 = not found, 401 = unauthorized, 429 = too many requests, 500 = server error) plus the data.
An API's request and response cycle Your script sends a GET request to the API; the API answers with status code 200 and JSON data containing base_code and rates. Your script API 1. GET 2. 200 + JSON {"base_code": "USD", "rates": {"EUR": …, "MXN": …}}
  • 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.
Every call to 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?

  • Everything went fine.
  • The URL doesn't exist.
  • You made too many requests in a short time.

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)
Never write your keys in your code

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?

  • Read it from an environment variable with os.environ.
  • Store it in a variable at the top of the file.
  • Put it in a comment.

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; suffix is 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.
Do a dry run first

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?

  • Nothing; that's what the Recycle Bin is for.
  • Do a dry run (just printing what it would do) on a copy of the folder.
  • Run it twice to be sure.

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?

  • Run the script with admin permissions
  • Build the path with Path(__file__).parent / "expenses.csv"
  • Change the time the task runs

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

  1. Run the project with your own expenses.csv and check the report.log file.
  2. Add a second sheet to the Excel file with spending by month (hint: pd.ExcelWriter, like in lesson 11).
  3. Schedule it to run every Monday at 8:00 a.m.
  4. 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 .env file, never in code.
  • pathlib and shutil automate file tasks; always do a dry run first.
  • Schedule scripts with Task Scheduler or cron, use Path(__file__).parent for paths and logging instead of print.
  • 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.

🎓 Save your progress and get a free certificate

Create your account in 30 seconds, mark your lessons and download your certificate at the end.

Enroll for free