No. The pandas documentation says it does not attempt to sanitize SQL statements and simply forwards the statement to the underlying driver. If you build the query with an f-string or string concatenation, injection is possible. Pass values through the params argument and let the driver bind them.
What pandas actually says
The Notes section of read_sql states that pandas does not attempt to sanitize SQL statements. It forwards the statement to the underlying driver, which may or may not sanitize from there, and the docs say to be wary when accepting statements from arbitrary sources. Separately, for parameterized queries, the page recommends using params over string interpolation.
That is a clear division of labour. Pandas is a conduit. Safety comes from how you build the query.
The unsafe pattern and the safe one
Analysts often write queries like this, with a value taken from a notebook widget, a web form or a config file:
# unsafe: the value becomes part of the SQL text
q = f"SELECT * FROM orders WHERE customer = '{name}'"
df = pd.read_sql(q, conn)
If name is controlled by someone else, they can close the quote and add SQL of their own. In a read-only role the damage may be limited to data exposure, but exposure is usually the point of an attack on an analytics database.
The safer form keeps values out of the SQL text:
from sqlalchemy import text
q = text("SELECT * FROM orders WHERE customer = :name")
df = pd.read_sql(q, conn, params={"name": name})
The page shows this style, with a SQLAlchemy text() query and a named placeholder.
Know your driver's paramstyle
The params argument is driver dependent. The pandas docs tell you to check your driver documentation for which of the five styles in PEP 249 it supports. Their example is psycopg2, which uses %(name)s placeholders with a dictionary. Other drivers use question marks or colon names. If you copy a query from one database to another, the placeholder syntax may change, and that is a common reason people fall back to f-strings. Resist it.
What parameters cannot do
Placeholders bind values. They do not bind table names, column names or sort directions. If a user picks the column to group by, do not paste it into the SQL. Check it against a fixed allow list in code, then insert the approved identifier.
allowed = {"region", "product", "month"}
if col not in allowed:
raise ValueError("unknown column")
A short review checklist
- Search the repository for read_sql calls whose first argument is built with an f-string, format or plus sign.
- Check where each variable comes from, including notebook inputs and scheduled job parameters.
- Replace values with bound parameters.
- Allow list any identifier that varies.
- Run the query with a database role that can only read what the job needs.
Try it yourself
Click through it at your own pace, or open it full screen. Look at how a scan flags user input reaching a query.
Where Safeguard fits
Safeguard offers code scanning and dependency scanning, and you can run a free scan on a repository that holds your analytics code.
Quick check
Does using SQLAlchemy make read_sql safe?
Only if you pass values as bound parameters. A SQLAlchemy engine does not protect a query string you built with an f-string.
Can parameters protect a table name?
No. Placeholders bind values only. Validate identifiers against an allow list.
Sources
Last reviewed: 19 October 2026.