A simple openpyxl field-scanning script pulled a whole thread of engineering decisions behind Excel-parsing engine selection: the underlying structure of XLSX files, openpyxl’s performance ceiling, how the Rust-written python-calamine replaces it in ETL pipelines, and finally the broader Polars ecosystem plus an actual code rewrite. This post follows the conversation’s progression start to finish.

Part 1: How openpyxl Reads Files — and the Interview-Level Deep Dive

🎯 The 30-Second Version

openpyxl doesn’t read Excel’s “now” — it reads the “then” of whenever the file was last saved. An XLSX file is really a ZIP archive full of XML documents (sheet1.xml, sharedStrings.xml, etc). openpyxl parses these into a Python object tree, and data_only=True gives you the formula result cached at last save time, not the formula itself. Using iter_rows as a generator to scan row by row and match substrings is essentially “structured grep.”

⚙️ Under the Hood

🔬 The Interviewer’s Follow-Up Chain

Q1: This xlsx has 500K rows and 200 columns. What happens with this code? A: The default mode builds the entire DOM tree in memory — easy OOM or a hang. The fix is load_workbook(path, data_only=True, read_only=True), which makes iter_rows a truly lazy generator — O(1) memory instead of O(rows × columns).

Q2: Can you still access row[0].row in read_only mode? What’s the catch? A: Yes, Cell.row/column still work. But read_only mode is not writable, and it doesn’t support random access (no ws['A100'] jumps) — only sequential iteration. You trade random access for constant memory.

Q3: What’s the complexity of joining each row into a string and doing substring matching? A: str(c.value) + join per row is O(columns), and substring search is O(string length). But materializing a full string every row means repeated allocations — under 500K rows the GC pressure is heavier than you’d think. Better to filter with a generator instead of building a full string every time.

Q4: Is it safe to read the same workbook concurrently from multiple threads? A: openpyxl’s Workbook object is not thread-safe. The right pattern is each worker calling load_workbook independently for its own read-only handle, not sharing one wb object across threads.

Q5: What’s the biggest failure mode here? A: Silently returning wrong data instead of erroring. data_only=True returning None, a mistyped sheet name causing KeyError, or a missing file causing FileNotFoundError — all need explicit try/except guards, or a silent None will leak into your matching logic.

🏗️ How Big Tech Actually Uses This

💸 The High-Stakes Version (Finance)

Correctness beats speed:

🌉 The Cross-Discipline Lens

This is like stratigraphy in archaeology: you’re not looking at “live” data, you’re excavating a “layer that solidified at the last save.” The cached formula value under data_only=True is like an insect trapped in amber — it precisely preserves one historical moment, but if the layer was disturbed afterward (formula recalculated but never re-saved), what you dig up may be a “ghost version” that no longer exists.


Part 2: The Engineering Logic Behind python-calamine Replacing openpyxl

🎯 The 30-Second Version

openpyxl is a pure-Python XML parser — reading a large xlsx means repeatedly building a DOM tree at the Python layer, which is slow and memory-hungry. calamine is a Rust-written xlsx/xls/ods parsing library exposed to Python via PyO3 bindings, pushing the most expensive part — XML parsing and string handling — down into compiled code. ETL is read-only, so there’s no reason to pay the pure-Python parsing tax for write-only features (formulas, styling) you’ll never use.

⚙️ Under the Hood

🔬 The Interviewer’s Follow-Up Chain

Q1: calamine is an order of magnitude faster — is that CPU or I/O bound? A: CPU-bound parsing overhead, not I/O. The xlsx file itself is small (ZIP-compressed), so I/O time is negligible. The bottleneck is turning XML text into structured data — object allocation and type checking — where Rust’s zero-cost abstractions and lack of GC crush CPython.

Q2: What does calamine do with formulas in the xlsx? A: It only reads cached values (similar to data_only=True), doesn’t evaluate formulas, and doesn’t expose the formula text either. If you need to see, edit, or store formulas, openpyxl remains the only option.

Q3: What happens with merged cells or multi-level headers when Polars uses the calamine engine? A: Merged cells only have a value in the top-left cell — the rest are None, same as openpyxl; you still have to forward-fill at the business layer. Type inference is stricter with calamine/Polars though — a column mixing strings and numbers tends to get coerced into one dtype, with mismatches becoming null or an error. This is the biggest gotcha when migrating “dirty” hand-filled spreadsheets.

Q4: How should a production normalization adapter be designed for fault tolerance? A: Three layers of defense: ① format detection (check magic bytes to catch mislabeled files); ② schema validation (pydantic/pandera checks column names/counts/types right after conversion — fail loudly instead of producing dirty parquet); ③ fallback strategy (retry with openpyxl if calamine fails on an edge case).

Q5: Memory-wise, which is leaner — calamine or openpyxl with read_only=True? A: calamine typically still loads the whole thing into memory (it has no streaming cursor mode like openpyxl’s read_only), but its underlying structures are more compact than Python objects, so memory usage is significantly lower than openpyxl’s full-DOM mode — though not necessarily lower than openpyxl’s streaming mode. For huge files under tight memory constraints, this needs a case-by-case trade-off.

🏗️ How Big Tech Actually Uses This

💸 The High-Stakes Version (Finance)

🚀 What’s Cutting-Edge in 2026

🌉 The Cross-Discipline Lens

Think of it as literal translation vs. compiled translation in translation theory. openpyxl is like a human simultaneous interpreter — parsing and rephrasing on the spot, word by word: accurate but slow, and bottlenecked by the interpreter’s (Python interpreter’s) own cognitive bandwidth. calamine is more like a professionally pre-trained machine translation engine — the work of “understanding structure” was already compiled down into a lower, faster system ahead of time. You just want the final translation; you don’t need that slow general-purpose interpreter showing up for every single step.


Part 3: The Polars Ecosystem, in Full

Polars isn’t just “pandas with a new skin” — it’s a full ecosystem spanning single-machine in-memory computation all the way to distributed cloud execution, built on Rust, the Arrow memory format, and lazy evaluation. pandas is a “manual bicycle”; Polars is a “turbocharged, self-driving car” — same road, completely different ride.

Ecosystem components:

Component Purpose Example
polars-core Core DataFrame engine, Rust-based, columnar Arrow memory df.group_by("region").agg(pl.col("revenue").sum())
Lazy API Lazy evaluation — builds a query plan before optimizing execution, similar to Spark’s DAG pl.scan_parquet("*.parquet").filter(...).collect()
Streaming Engine Handles data larger than memory by processing in batches .collect(streaming=True)
IO layer Reads/writes parquet/csv/json/xlsx(calamine)/Delta Lake/Iceberg pl.read_excel("f.xlsx", engine="calamine")
polars-cloud Submits local lazy queries seamlessly to a distributed cloud cluster Local code + .remote() runs on a cluster
connectorx integration Pulls data straight from databases (Postgres/MySQL/Snowflake) into Polars, several times faster than pandas.read_sql pl.read_database_uri(query, uri)
Arrow interop Zero-copy data exchange with DuckDB, PyArrow, Ray pl.from_arrow(duckdb_result.arrow())

More hands-on examples:

import polars as pl

# Example 1: Lazy query + predicate pushdown
# Nothing executes until collect() is called; Polars automatically pushes
# the filter down as close to the read as possible.
result = (
    pl.scan_parquet("s3://bucket/events/*.parquet")
    .filter(pl.col("event_date") >= "2026-01-01")
    .group_by("user_id")
    .agg(pl.col("amount").sum().alias("total_spend"))
    .sort("total_spend", descending=True)
    .limit(100)
    .collect()
)

# Example 2: Window functions (equivalent to SQL's OVER PARTITION BY)
df = df.with_columns(
    pl.col("revenue").rank(descending=True).over("region").alias("rank_in_region")
)

# Example 3: Zero-copy interop with DuckDB, playing to each tool's strengths
import duckdb
arrow_tbl = df.to_arrow()
duckdb.sql("SELECT region, SUM(revenue) FROM arrow_tbl GROUP BY region")

Part 4: Rewriting the Original Scanning Script with calamine

The original task was scanning multiple sheets for rows containing specific field names. Here are two versions: plain python-calamine (simplest, leanest memory footprint) and Polars with the calamine engine (better if you’re chaining filtering/aggregation logic afterward).

Version A: Plain python-calamine

from python_calamine import CalamineWorkbook

path = '/Users/toddzhang/ws/mq/uac/docs/FTG-083-UAC_to_SFEC_Full_Mapping_v2 _mask_fields_revised.xlsx'

wb = CalamineWorkbook.from_path(path)

for sheet_name in ['PersonAccount', 'UAC-SFBuildCheck']:
    print('====', sheet_name)
    sheet = wb.get_sheet_by_name(sheet_name)
    rows = sheet.to_python()  # list[list[Any]], parsed into native Python types in one shot

    for row_idx, row in enumerate(rows[:80], start=1):
        vals = [str(v) for v in row if v is not None]
        joined = ' | '.join(vals)
        if 'UacApplicantId' in joined or 'external' in joined.lower():
            print(f'r{row_idx}: {joined[:400]}')

Key differences from openpyxl:

import polars as pl

path = '/Users/toddzhang/ws/mq/uac/docs/FTG-083-UAC_to_SFEC_Full_Mapping_v2 _mask_fields_revised.xlsx'

for sheet_name in ['PersonAccount', 'UAC-SFBuildCheck']:
    print('====', sheet_name)
    df = pl.read_excel(
        path,
        sheet_name=sheet_name,
        engine="calamine",
        read_options={"header_row": None},  # original code didn't assume a header row, keep parity
    )

    # Concatenate each row into a single string column, then use a Polars
    # expression for substring matching (vectorized, not a Python for-loop)
    joined_expr = pl.concat_str(
        [pl.col(c).cast(pl.Utf8) for c in df.columns],
        separator=' | ',
        ignore_nulls=True,
    )

    matched = (
        df.with_row_index("row_num", offset=1)
        .with_columns(joined_expr.alias("joined"))
        .filter(
            pl.col("joined").str.contains("UacApplicantId")
            | pl.col("joined").str.to_lowercase().str.contains("external")
        )
        .head(80)
    )

    for row in matched.iter_rows(named=True):
        print(f"r{row['row_num']}: {row['joined'][:400]}")

The value of Version B: string matching moves from a Python-level row-by-row loop to a vectorized string operation inside Polars’ expression engine (backed by Rust’s str.contains, SIMD-accelerated). At the million-row scale, the original openpyxl version is likely seconds to tens of seconds of a Python for-loop, whereas the Polars-expression version runs in milliseconds.

Which to pick: for a quick field-name scan across a few sheets, Version A is simplest with the fewest dependencies. For a real ETL/validation pipeline that later needs groupby/join/diff, go straight to Version B and skip an extra layer of glue code.


Summary

One-line mic-drop: calamine isn’t fast because “Rust beats Python” as some kind of folklore — it’s fast because it drops the number of cross-language boundary crossings from once per cell to once per sheet. The essence of engineering optimization is always reducing boundary crossings, not rewriting logic in a different language.