> ## Documentation Index
> Fetch the complete documentation index at: https://jetxl.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Data types

> How Arrow and DataFrame types land in spreadsheet cells

Jetxl reads Arrow arrays directly, so what lands in a cell depends on the Arrow type of the column rather than on the Python object you started with.

## Supported types

| Arrow type                            | Becomes           |
| ------------------------------------- | ----------------- |
| `Int8`, `Int16`, `Int32`, `Int64`     | A number          |
| `UInt8`, `UInt16`, `UInt32`, `UInt64` | A number          |
| `Float32`, `Float64`                  | A number          |
| `Boolean`                             | `TRUE` or `FALSE` |
| `Utf8`, `LargeUtf8`                   | Text              |
| `Date32`, `Date64`                    | A date            |
| `Time32`, `Time64`                    | A time            |
| `Timestamp`                           | A date and time   |
| `Null`                                | An empty cell     |

## Types Jetxl converts for you

Three common cases would otherwise be rejected, so Jetxl normalizes them once per batch before writing.

<AccordionGroup>
  <Accordion title="Categorical columns" icon="tags">
    A pandas `Categorical`, or a Polars `Categorical` or `Enum`, arrives as an Arrow `Dictionary` array. Jetxl decodes it to the underlying value type, usually `Utf8`, and writes it as text.
  </Accordion>

  <Accordion title="Arrow view types" icon="eye">
    `Utf8View` and `BinaryView` are cast to `Utf8` and `Binary`. You meet these when you pass a Polars DataFrame straight in rather than calling `.to_arrow()`.
  </Accordion>

  <Accordion title="Everything already concrete" icon="forward">
    Columns that are neither dictionary-encoded nor view types pass through untouched, so PyArrow and pandas workloads pay only a cheap scan.
  </Accordion>
</AccordionGroup>

<Tip>
  Because view types are handled, a bare Polars DataFrame works without conversion:

  ```python theme={null}
  jet.write_sheet_arrow(df, "output.xlsx")     # no .to_arrow()
  ```

  The examples on this site call `.to_arrow()` explicitly, which is clearer about what's being passed and works identically for pandas.
</Tip>

## Unsupported types

Jetxl checks the schema once per sheet, before writing anything, and raises an `OSError` naming the offending column:

```text theme={null}
Column 'payload' has unsupported Arrow type Struct(...). Supported types:
integers, floats, boolean, Utf8/LargeUtf8, Date32/64, Time32/64, and
Timestamp. Cast the column before writing.
```

This is a catchable error rather than a silent empty column, and the check costs nothing measurable because it runs per column rather than per cell.

Nested types are the usual cause. A struct, list or map column has no single-cell representation in a spreadsheet, so flatten or serialize it first:

```python theme={null}
df = df.with_columns(
    pl.col("payload").struct.field("id").alias("payload_id"),
    pl.col("tags").list.join(", ").alias("tags"),
).drop("payload")
```

## Values that change on the way out

Three conversions happen silently. None of them raises, so the file writes successfully and the difference only shows when you open it.

<Warning>
  **`NaN` and infinity become empty cells.** Neither is a valid numeric value in the spreadsheet format, and writing one makes the whole workbook unreadable, so Jetxl writes an empty cell instead. This applies on both the Arrow and dictionary paths.

  If a `NaN` in your data means something, convert it before writing:

  ```python theme={null}
  df = df.with_columns(pl.col("score").fill_nan(0.0))
  ```
</Warning>

<Warning>
  **Control characters are stripped from text.** Bytes in the C0 control range, other than tab, newline and carriage return, have no legal representation in the underlying XML at all. Jetxl drops them rather than producing a file Excel rejects. This matches XlsxWriter's behavior.

  Text arriving from scraped HTML or legacy systems is where this shows up.
</Warning>

<Note>
  **Nulls become empty cells**, not the text `"None"`, `"NaN"` or `"null"`. An empty cell is not zero: Excel's `AVERAGE` skips empty cells but includes zeros, so a null-heavy column averages differently depending on whether you filled the gaps before writing.
</Note>

## Grid limits

Excel's own ceiling is 1,048,576 rows and 16,384 columns. Jetxl checks both before writing and raises rather than producing a truncated file.

<Warning>
  The row limit counts the header. A frame of exactly 1,048,576 rows written with `write_header_row=True` is one row over and fails. Set `write_header_row=False`, or split the export.
</Warning>

For data beyond the limit, split across sheets with [`write_sheets_arrow`](/reference/multi-sheet-functions), or reconsider whether a spreadsheet is the right format. At that size a CSV or Parquet file serves most readers better.

## Dates

`Date32` counts days from 1 January 1970, and Jetxl converts that to the serial number Excel expects. The cell holds a real date, not text, so Excel sorts and filters it correctly.

Excel's date system includes a leap day, 29 February 1900, that never existed. Jetxl accounts for it, so dates before 1 March 1900 land on the right day rather than one day out. Historical data is safe.

A date still needs a format to display readably. Without one you see the underlying serial number:

```python theme={null}
jet.write_sheet_arrow(
    df.to_arrow(),
    "dated.xlsx",
    column_formats={
        "created_at": "yyyy-mm-dd",   # custom code, same in every locale
        "processed_at": "datetime",   # yyyy-mm-dd hh:mm:ss
    },
)
```

The built-in `date` name maps to Excel's short-date format, which renders by the reader's locale. Pass the custom code when you need a fixed appearance.

See [Number formats](/guides/number-formats) for the full list.
