snowflake-connector-python
Snowflake's own Python driver, and the layer almost everything else in the Snowflake Python world sits on: Snowpark, SQLAlchemy, dbt-snowflake and the Airflow provider all import it. It implements the DB API 2.0 interface, so connect, cursor, execute, fetchall look like any other database driver, but it speaks Snowflake's HTTPS protocol rather than a wire protocol, needs no JDBC or ODBC, and adds Snowflake-specific work on top: result sets arrive as Arrow batches that convert straight into pandas DataFrames, PUT and GET move files to and from stages with multipart upload and encryption, queries can be fired asynchronously and collected later by query ID, and every authentication method Snowflake supports (password, key pair, OAuth, SSO, workload identity federation on AWS, Azure and GCP) is implemented here. Version 4 added an asyncio client under snowflake.connector.aio.
The default and correct choice for talking to Snowflake from Python, kept current by Snowflake itself. Budget for a heavy install, plan key pair authentication and a connections.toml from the start, and reach for Snowpark instead if your code is mostly DataFrame transformations rather than SQL.
Use it if
- You run SQL against Snowflake from Python and want the vendor-maintained driver rather than a community wrapper, with same-week support for new authentication methods and server features
- You move result sets into pandas or Arrow: fetch_pandas_all() and fetch_arrow_all() read the Arrow result format directly instead of building DataFrames row by row
- You load DataFrames back into tables, where write_pandas() handles chunking, staging, compression and COPY INTO for you
- You need long-running queries to survive your process: execute_async() returns a query ID you can poll or pick up from a completely different process with get_results_from_sfqid()
- You authenticate with anything other than a password, such as key pair with an encrypted private key, OAuth with token refresh, or workload identity on AWS, Azure or GCP
- You want a small dependency footprint. It pulls cryptography, pyOpenSSL, pyjwt, boto3 and botocore by default, vendors its own urllib3, and ships a compiled Rust component (sf_mini_core). Adding the pandas extra brings pyarrow, which is hundreds of megabytes unpacked. Setting SNOWFLAKE_NO_BOTO at install time drops boto3 but you lose the S3 stage paths that depend on it
- You mostly write DataFrame transformations rather than SQL. snowflake-snowpark-python gives you a DataFrame API that pushes computation into the warehouse instead of pulling rows to your machine, and it uses this connector underneath anyway
- You need GCP regional endpoints. The README states outright that the library does not support them
- You are on Python 3.9 or older. Support was dropped in 4.6.0 and the floor is now 3.10
- You object to default telemetry. The connector sends usage data and one per-login record describing the shape of your connection identifiers unless you disable CLIENT_TELEMETRY_ENABLED or set the connector's own opt-out variables
- You want ORM-style access. This is a raw DB API driver; SQLAlchemy support is a separate snowflake-sqlalchemy package with its own release cadence and dialect quirks
Setup reality
pip install snowflake-connector-python is straightforward but not small, and pip install "snowflake-connector-python[pandas]" adds pyarrow and pandas on top, which is what most data teams actually need. Expect a slow install and a large image layer. Connecting is where the real work is. Password auth is being retired for human users at Snowflake, so most production setups use key pair, which means generating an RSA key, running ALTER USER SET RSA_PUBLIC_KEY, and passing private_key_file with private_key_file_pwd for an encrypted key. Stop putting credentials in code and use a connections.toml file with connect(connection_name="prod"): the connector reads it from the platform config directory and, since 4.0, refuses to read it if the file is group or world writable, which is a ConfigSourceError that surprises people in containers. Certificate revocation checking (OCSP and now CRL) runs on connect and is the usual cause of hangs behind a corporate proxy or in a locked-down VPC; give the connector real network access to Snowflake's OCSP endpoints rather than working around it. Two more: v4.0 made DictCursor no longer a subclass of SnowflakeCursor (use SnowflakeCursorBase for isinstance checks and type hints), and the default paramstyle is pyformat, so %s and %(name)s in your SQL are placeholders whether you meant them to be or not.
Patterns
Connect and run a queryconnect-and-query
import snowflake.connector
with snowflake.connector.connect(
account="myorg-myaccount",
user="SVC_ETL",
private_key_file="/secrets/rsa_key.p8",
warehouse="COMPUTE_WH",
database="ANALYTICS",
schema="PUBLIC",
role="ETL_ROLE",
) as conn:
with conn.cursor() as cur:
cur.execute("SELECT current_version()")
print(cur.fetchone()[0])Both the connection and the cursor are context managers. Closing the connection matters: an abandoned session keeps a warehouse from auto-suspending, which costs credits.
Keep credentials out of codeconnections-toml
# ~/.snowflake/connections.toml
# [prod]
# account = "myorg-myaccount"
# user = "SVC_ETL"
# authenticator = "SNOWFLAKE_JWT"
# private_key_file = "/secrets/rsa_key.p8"
# warehouse = "COMPUTE_WH"
import snowflake.connector
conn = snowflake.connector.connect(connection_name="prod")Since 4.0 the connector raises ConfigSourceError if connections.toml is writable by group or others; chmod 600 it inside containers or the connection never opens.
Bind parameters instead of formatting SQLbind-parameters
cur.execute(
"SELECT id, email FROM users WHERE created_at >= %s AND status = %s",
(start_date, "active"),
)
# named binding
cur.execute(
"SELECT * FROM orders WHERE region = %(region)s",
{"region": "EMEA"},
)The default paramstyle is pyformat, so any literal % in your SQL (a LIKE '%x%' pattern, for example) must be escaped as %% or execute() fails to interpolate.
Get rows as dictionariesdict-cursor
from snowflake.connector import DictCursor
with conn.cursor(DictCursor) as cur:
for row in cur.execute("SELECT id, email FROM users LIMIT 5"):
print(row["ID"], row["EMAIL"])Keys come back in Snowflake's uppercase form unless the columns were created quoted. Since v4.0 DictCursor is not a subclass of SnowflakeCursor; annotate against SnowflakeCursorBase if you type-hint cursors.
Read a result set into pandas or Arrowfetch-pandas
# pip install "snowflake-connector-python[pandas]"
cur.execute("SELECT * FROM daily_revenue WHERE dt >= '2026-01-01'")
df = cur.fetch_pandas_all()
# for results too large for memory:
for chunk in cur.fetch_pandas_batches():
process(chunk)These methods only work when the server returns the Arrow result format and the pandas extra is installed; without it you get a ProgrammingError telling you to install the optional dependency.
Load a DataFrame into a tablewrite-pandas
from snowflake.connector.pandas_tools import write_pandas
success, nchunks, nrows, _ = write_pandas(
conn,
df,
table_name="DAILY_REVENUE",
database="ANALYTICS",
schema="PUBLIC",
quote_identifiers=True,
auto_create_table=False,
overwrite=False,
)With quote_identifiers=True (the default) your DataFrame column names must match the table's case exactly. write_pandas stages Parquet files and runs COPY INTO, so the role needs stage privileges as well as insert.
Insert many rows in one round tripbulk-insert
rows = [(1, "a"), (2, "b"), (3, "c")]
cur.executemany(
"INSERT INTO events (id, name) VALUES (%s, %s)",
rows,
)
conn.commit()executemany rewrites a single INSERT ... VALUES into one batched statement; for anything above roughly a hundred thousand rows, stage a file and COPY INTO instead.
Fire a long query and collect it laterasync-query
cur.execute_async("CALL rebuild_marts()")
query_id = cur.sfqid
# later, possibly in another process
with conn.cursor() as cur2:
while conn.is_still_running(conn.get_query_status(query_id)):
time.sleep(5)
conn.get_query_status_throw_if_error(query_id)
cur2.get_results_from_sfqid(query_id)
print(cur2.fetchall())get_query_status alone never raises on a failed query; call get_query_status_throw_if_error or you will silently treat a failure as an empty result.
Use the connector from asyncioasyncio-client
import asyncio
from snowflake.connector.aio import SnowflakeConnection
async def main():
async with SnowflakeConnection(connection_name="prod") as conn:
async with conn.cursor() as cur:
await cur.execute("SELECT current_warehouse()")
print(await cur.fetchone())
asyncio.run(main())snowflake.connector.aio is the async client added in v4; the synchronous snowflake.connector API blocks the event loop, so do not mix them in async code.
Move files to and from a stagestage-file-transfer
cur.execute("PUT file:///data/events_*.csv.gz @%events AUTO_COMPRESS=FALSE PARALLEL=8")
cur.execute(
"COPY INTO events FROM @%events FILE_FORMAT = (TYPE = CSV) PURGE = TRUE"
)
cur.execute("GET @%events file:///tmp/download/")PUT and GET are executed by the driver, not the server, so local paths are resolved on the machine running Python and file:// URIs are required.
Set session behavior and disable telemetrysession-parameters
conn = snowflake.connector.connect(
connection_name="prod",
autocommit=False,
client_session_keep_alive=True,
session_parameters={
"QUERY_TAG": "nightly-etl",
"STATEMENT_TIMEOUT_IN_SECONDS": 900,
"CLIENT_TELEMETRY_ENABLED": False,
},
)QUERY_TAG is the cheapest way to attribute warehouse spend later, and STATEMENT_TIMEOUT_IN_SECONDS is the only reliable guard against a runaway query burning credits.
Catch Snowflake errors specificallyhandle-errors
from snowflake.connector.errors import (
ProgrammingError,
OperationalError,
DatabaseError,
)
try:
cur.execute("SELECT * FROM does_not_exist")
except ProgrammingError as e:
print(e.errno, e.sqlstate, e.sfqid, e.msg)
except OperationalError:
raiseProgrammingError covers SQL compilation and permission failures; every error carries sfqid, which is the query ID to hand to Snowflake support or paste into query history.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| snowflake-snowpark-python | PyPI | You want a DataFrame API that runs the work inside Snowflake instead of pulling rows into your process |
| snowflake-sqlalchemy | PyPI | You need a SQLAlchemy dialect for ORM models, Alembic migrations or existing SQLAlchemy code |
| adbc-driver-snowflake | PyPI | You want Arrow-native bulk reads through the ADBC interface with a much smaller Python dependency tree |