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

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions