Data Access Libraries (SQLAlchemy & Requests)
Learn how Python data science projects pull data in from SQL databases with SQLAlchemy and from REST APIs with requests.
Introduction
Before you can clean, analyze, or model anything, the data has to get into Python in the first place. So far in this course that has mostly meant reading a CSV file. Real projects usually pull data from somewhere live: a production database, or a REST API run by a third party. This lesson covers the two most common libraries for exactly that.
- How SQLAlchemy lets Python talk to SQL databases, both with raw Core queries and with the higher-level ORM.
- How to pull data out of a database directly into a pandas DataFrame.
- How requests fetches data from REST APIs and parses JSON responses.
sqlalchemy: Talking to SQL Databases
SQLAlchemy is the standard way Python code talks to relational databases — PostgreSQL, MySQL, SQLite, and others — through one consistent interface. It has two layers you will see referenced constantly: Core, which lets you build and run SQL queries directly, and the ORM (Object-Relational Mapper), which lets you represent database tables as Python classes and rows as Python objects.
pip install sqlalchemyThe Core style is the fastest way to run a query and get data into pandas — it is what you will use most often for pure data science work, since pandas.read_sql() accepts a SQLAlchemy engine directly.
import pandas as pdfrom sqlalchemy import create_engine
engine = create_engine("sqlite:///sales.db")
df = pd.read_sql("SELECT * FROM orders WHERE amount > 100", engine)print(df.head())The ORM style is more common in application code that reads and writes individual records — for example, a FastAPI backend around a model you trained. It maps a table to a Python class.
from sqlalchemy import create_engine, Column, Integer, String, Floatfrom sqlalchemy.orm import declarative_base, Session
engine = create_engine("sqlite:///sales.db")Base = declarative_base()
class Order(Base): __tablename__ = "orders" id = Column(Integer, primary_key=True) customer = Column(String) amount = Column(Float)
with Session(engine) as session: big_orders = session.query(Order).filter(Order.amount > 100).all() for order in big_orders: print(order.customer, order.amount)Click Run to see what this code prints.
For pulling data into a DataFrame for analysis, prefer the Core style with pandas.read_sql() — it is shorter and exactly what pandas expects. Reach for the ORM when you are writing an application that creates, updates, and deletes individual records, not just reading bulk data for analysis.
requests: Talking to REST APIs
A huge amount of real-world data lives behind REST APIs rather than in a database you can query directly — weather data, stock prices, public government datasets, internal company services. requests is the standard Python library for making HTTP calls: GET to fetch data, POST to send it, along with headers, authentication, and query parameters.
pip install requestsimport requests
response = requests.get( "https://api.example.com/v1/exchange-rates", params={"base": "USD"}, headers={"Authorization": "Bearer YOUR_API_KEY"},)
response.raise_for_status() # raises an exception on 4xx/5xx errorsdata = response.json()
print(data["rates"]["EUR"])Click Run to see what this code prints.
It is common to combine requests with pandas to turn an API response straight into a DataFrame for analysis.
import pandas as pdimport requests
response = requests.get("https://api.example.com/v1/orders")response.raise_for_status()
orders = response.json()["results"]df = pd.DataFrame(orders)print(df.head())A failed request does not automatically raise an exception in requests — a 404 or 500 response still returns normally. Always call response.raise_for_status(), or explicitly check response.status_code, before trusting response.json().
Common Mistakes
- Building SQL query strings by hand with string concatenation instead of using SQLAlchemy parameter binding — this opens the door to SQL injection.
- Forgetting response.raise_for_status(), so failed API calls silently return an empty or error payload that gets parsed as if it were real data.
- Opening a new database connection per query instead of reusing a single engine, which SQLAlchemy pools for you automatically.
Best Practices
- Use pandas.read_sql() with a SQLAlchemy engine for analysis workloads, and the ORM for application-style read/write code.
- Store database URLs and API keys in environment variables with python-dotenv, never hardcoded.
- Always set a timeout on requests calls (requests.get(url, timeout=10)) so a hung server does not freeze your script.
- Use requests.Session() when making many calls to the same API, to reuse the underlying TCP connection.
Frequently Asked Questions
You still need it under the hood — pandas.read_sql() requires a SQLAlchemy engine (or a raw DBAPI connection) to know how to talk to your specific database.
requests remains the most widely used and most battle-tested HTTP library in Python. httpx is a newer alternative that adds async support and an HTTP/2 client, worth considering if you specifically need asynchronous requests.
Yes — PostgreSQL, MySQL, SQL Server, Oracle, and more, each through a small driver package (like psycopg2 for PostgreSQL) alongside sqlalchemy itself.
Key Takeaways
- SQLAlchemy Core is the fastest path from a SQL database into a pandas DataFrame.
- SQLAlchemy ORM maps tables to Python classes for application-style read/write code.
- requests is the standard way to call REST APIs and parse JSON responses in Python.
- Always validate API responses with raise_for_status() before trusting the returned data.
Summary
SQLAlchemy and requests cover the two most common ways real data enters a Python project: pulled from a database, or fetched from an API. Next, we look at a third source — scraping data directly out of web pages, using BeautifulSoup and Scrapy.
- You can query a SQL database with SQLAlchemy Core and ORM.
- You can fetch and parse JSON data from a REST API with requests.
- You are ready to look at web scraping libraries next.