Relational databases often hold the authoritative copy of operational data, while pandas provides a convenient place to filter and reshape selected rows. Moving data through a DataFrame keeps that analysis inside Python without introducing an intermediate CSV export.
The local SQLite database used here is disposable, but the SQLAlchemy engine follows the same connection pattern that pandas uses with PostgreSQL, MySQL, and other supported databases. The script reads a filtered result, calculates tax values, and writes those values to a separate results table.
The read_sql() call accepts a table name or SQL query, and DataFrame.to_sql() writes rows according to its if_exists policy. Bound parameters keep query values separate from SQL text; table names and replacement policies must remain controlled application settings.
Related: How to read CSV files with pandas
Related: How to write a CSV file with pandas
Related: How to read and write Parquet files with pandas
$ python3 -m pip install SQLAlchemy
SQLite support is included with Python, while SQLAlchemy supplies a consistent database connection interface.
Related: How to install pandas with pip
from pathlib import Path import pandas as pd from sqlalchemy import create_engine, text database = Path("orders.db") engine = create_engine(f"sqlite:///{database}") source_orders = pd.DataFrame( { "order_id": ["A100", "A101", "A102"], "customer": ["Ada", "Lin", "Mira"], "status": ["open", "closed", "open"], "total_usd": [125.50, 88.00, 212.25], } ) source_orders.to_sql("orders", engine, if_exists="replace", index=False)
if_exists=“replace” drops an existing table before inserting rows. This policy is appropriate only for a disposable input table or a target that is explicitly safe to rebuild.
minimum_total = 100 query = text( """ SELECT order_id, customer, total_usd FROM orders WHERE status = :status AND total_usd >= :minimum_total ORDER BY order_id """ ) open_orders = pd.read_sql( query, engine, params={"status": "open", "minimum_total": minimum_total}, )
Bound parameters protect values such as status and minimum_total from string interpolation. Identifiers such as table and column names must remain in reviewed application code because SQL parameters do not bind identifiers.
open_orders["tax_usd"] = (open_orders["total_usd"] * 0.06).round(2)
with engine.begin() as connection: open_orders.to_sql( "open_order_totals", connection, if_exists="replace", index=False, )
The engine.begin() context commits the table write when the block succeeds and rolls it back when the database reports an error.
saved_orders = pd.read_sql( text( """ SELECT order_id, customer, total_usd, tax_usd FROM open_order_totals ORDER BY order_id """ ), engine, ) print(saved_orders.to_string(index=False)) engine.dispose()
$ python3 sql_roundtrip.py
order_id customer total_usd tax_usd
A100 Ada 125.50 7.53
A102 Mira 212.25 12.74
The two rows come from a new query against open_order_totals after the transaction has committed, so missing rows or incorrect calculations remain visible.