Skip to content

SNOW-3556267: Session.write_pandas raises AttributeError: 'list' object has no attribute 'upper' for Snowpark pandas DataFrames above PandasToSnowflakeParquetThresholdBytes #4228

Description

@JonathanStefanov

When you write a Snowpark pandas (Modin) DataFrame back to a table via Session.write_pandas, Snowpark picks one of two upload strategies based on a memory threshold:

  • ≤ 3 MB (default PandasToSnowflakeParquetThresholdBytes): switch the frame onto the Snowflake-pushdown backend (SnowflakeQueryCompiler) and INSERT. Works fine.
  • 3 MB: skip the round trip and upload a Parquet file directly via the Snowflake connector. Broken. Crashes with AttributeError: 'list' object has no attribute 'upper'.

Root cause is in pandas_to_snowflake (src/snowflake/snowpark/modin/plugin/extensions/utils.py:839): it passes a list[str] to a helper annotated as taking str. Mypy already detects
this but the file is just excluded from the pre-commit mypy whitelist. Full trace, reproducer, and proposed fix below.


  1. What version of Python are you using?

The bug originates inside a Snowflake stored procedure declared with RUNTIME_VERSION = '3.11'.
End-to-end reproduction and trace done against current main of repo.

Python 3.14.0 (v3.14.0:ebf955df7a8, Oct 7 2025, 08:20:14) [Clang 16.0.0 (clang-1600.0.26.6)]


  1. What operating system and processor architecture are you using?

macOS-26.3.1-arm64-arm-64bit-Mach-O

Production failure environment: Snowflake-managed compute.


  1. What are the component versions in the environment (pip freeze)?

The bug runs inside a Snowflake stored procedure so there is no local pip freeze for the failing environment. Stored procedure declaration:

RUNTIME_VERSION = '3.11'
PACKAGES = ('snowflake-snowpark-python', 'modin')


  1. What did you do?
  import modin.pandas as pd
  import snowflake.snowpark.modin.plugin  # noqa: F401
  from snowflake.snowpark import Session
  from snowflake.snowpark.modin.config.envvars import (
      PandasToSnowflakeParquetThresholdBytes,
  )

  session = Session.builder.create()

  # Force the Parquet path. In production this triggers naturally once the
  # DataFrame's shallow memory usage exceeds 3,000,000 bytes (the default
  # value of PandasToSnowflakeParquetThresholdBytes).
  PandasToSnowflakeParquetThresholdBytes.put(0)

  df = pd.DataFrame({"a": [1, 2, 3], "b": ["x", "y", "z"]})
  
  session.write_pandas(df, "TEST_TABLE", auto_create_table=True, overwrite=True)

  1. What did you expect to see?

Expected

DataFrame written to TEST_TABLE.

Actual

  AttributeError: 'list' object has no attribute 'upper'
    File "snowflake/snowpark/modin/plugin/extensions/utils.py", line 727,
         in _convert_to_snowflake_table_name_to_write_pandas_table_name
      return name.upper()

Full call trace

Layer 1
Session.write_pandas (src/snowflake/snowpark/session.py:3384). Dispatcher at line 3554-3557:

  modin_pandas, modin_is_imported = import_or_missing_modin_pandas()
  if modin_is_imported and isinstance(df, (modin_pandas.DataFrame, modin_pandas.Series)):
      self._write_modin_pandas_helper(df, table_name, location,
                                      database=database, schema=schema, ...)

Modin DataFrame → enters the Modin path.

Layer 2
Session._write_modin_pandas_helper (session.py:3307). Builds a structured fully-qualified name at line 3363-3367:

  name = [table_name]                           # ['TEST_TABLE']
  if schema:
      name = [quote_id(schema)] + name          # ['"PUBLIC"', 'TEST_TABLE']
  if database: 
      name = [quote_id(database)] + name        # ['"DB"', '"PUBLIC"', 'TEST_TABLE']

Then at line 3375:

df.to_snowflake(name=name, ...) # passes the list
Layer 3
to_snowflake dispatch. Local-pandas-backend DataFrames route to pandas_to_snowflake, registered at
src/snowflake/snowpark/modin/plugin/extensions/dataframe_extensions.py:81:

register_dataframe_accessor("to_snowflake", backend="Pandas")(pandas_to_snowflake)
Layer 4
pandas_to_snowflake (src/snowflake/snowpark/modin/plugin/extensions/utils.py:730). Signature: name: str | Iterable[str]. Threshold gate at line 759-779:

  if memory_usage <= PandasToSnowflakeParquetThresholdBytes.get():
      return self.set_backend("Snowflake").to_snowflake(name=name, ...)   # PATH A handles list correctly
  # PATH B (Parquet upload) is the buggy one

  Above threshold → falls through to line 839:
  
pd.session.write_pandas(
      ...,
      table_name=_convert_to_snowflake_table_name_to_write_pandas_table_name(name), # Bug will happen here
      auto_create_table=True,
      overwrite=if_exists != "append",
      table_type=table_type,
  )

The full name list is passed where a str is expected.

Layer 5
_convert_to_snowflake_table_name_to_write_pandas_table_name (utils.py:707-727). Signature: name: str:

def _convert_to_snowflake_table_name_to_write_pandas_table_name(name: str) -> str:
    if is_valid_snowflake_quoted_identifier(name):
        return unquote_name_if_quoted(name)
    else: 
        return name.upper()      # 'list' object has no attribute 'upper'

is_valid_snowflake_quoted_identifier(['TEST_TABLE']): len = 1 < 2 → False → falls to name.upper() → AttributeError.

Type system already detects this

Running mypy directly on the file:

  src/snowflake/snowpark/modin/plugin/extensions/utils.py:839: error:
      Argument 1 to "_convert_to_snowflake_table_name_to_write_pandas_table_name"
      has incompatible type "str | Iterable[str]"; expected "str"  [arg-type]

Proposed fix

In pandas_to_snowflake, normalize name to a list of parts, extract table / schema / database individually, pass each as a separate keyword argument to write_pandas (which already
accepts database= and schema=):

  name_parts = [name] if isinstance(name, str) else list(name)
  table_name_converted = _convert_to_snowflake_table_name_to_write_pandas_table_name(name_parts[-1])
  schema_converted = (
      _convert_to_snowflake_table_name_to_write_pandas_table_name(name_parts[-2])
      if len(name_parts) >= 2 else None
  )
  database_converted = (
      _convert_to_snowflake_table_name_to_write_pandas_table_name(name_parts[0])
      if len(name_parts) >= 3 else None
  )
  
  pd.session.write_pandas(
      ...,
      table_name=table_name_converted,
      database=database_converted,
      schema=schema_converted,
      ...
  )

  1. Can you set logging to DEBUG and collect the logs?

2026-05-18 14:14:10,496 - MainThread utils.py:1014 - enabled() - INFO - AST state has not been set explicitly. Defaulting to ast_enabled = True.
UserWarning: Snowpark pandas now runs with hybrid execution enabled by default, and will perform certain operations on smaller data using local, in-memory pandas. To disable this
behavior and force all computations to occur in Snowflake, run this line:
from modin.config import AutoSwitchBackend; AutoSwitchBackend.disable()
backend: NativeQueryCompiler
memory_usage: 1732 bytes
threshold forced to: 0
Traceback (most recent call last):
File "/Users/jonathanstefanov/Dev/snowpark-python/repro_with_logs.py", line 43, in
pandas_to_snowflake(df, name=["STIM_BLOCKS"], if_exists="replace", index=False)
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "src/snowflake/snowpark/modin/plugin/extensions/utils.py", line 839, in pandas_to_snowflake
table_name=_convert_to_snowflake_table_name_to_write_pandas_table_name(name),
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
File "src/snowflake/snowpark/modin/plugin/extensions/utils.py", line 727, in _convert_to_snowflake_table_name_to_write_pandas_table_name
return name.upper()
^^^^^^^^^^
AttributeError: 'list' object has no attribute 'upper'

Metadata

Metadata

Labels

bugSomething isn't workingstatus-triage_doneInitial triage done, will be further handled by the driver team

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions