openpyxl worksheet rows validated into pydantic models offline: working versions (tested 2026-10-16)
Read xlsx rows from openpyxl into pydantic v2 models, handling None cells, whole-float to int coercion, whitespace, ignored extra columns and row-numbered errors, all tested offline.
passed6 passed · 0 failed2026-10-16 00:56 UTC- Status
- passed
- Last passed
- 2026-10-16 00:56 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38832319459
The code
Exactly the files that passed. Your agent can pull them with get_recipe("openpyxl-pydantic-row-import") (install), or see them on GitHub.
openpyxl==3.1.5 pydantic==2.14.1 pytest==9.1.1
from datetime import date
from typing import Any
from openpyxl import load_workbook
from pydantic import BaseModel, ConfigDict, Field, ValidationError
class Product(BaseModel):
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
sku: str
name: str
quantity: int = Field(ge=0)
price: float
added: date | None = None
def rows_to_products(worksheet) -> list[Product]:
rows = worksheet.iter_rows(values_only=True)
header = next(rows)
columns = {cell: i for i, cell in enumerate(header) if isinstance(cell, str)}
products = []
for row_number, row in enumerate(rows, start=2):
raw = {name: row[i] if i < len(row) else None for name, i in columns.items()}
try:
products.append(Product.model_validate(raw))
except ValidationError as exc:
raise ValueError(f"row {row_number}: {exc}") from exc
return products
def load_products(path: Any) -> list[Product]:
workbook = load_workbook(path, read_only=True, data_only=True)
try:
return rows_to_products(workbook.active)
finally:
workbook.close()
from datetime import date
import io
import pytest
from openpyxl import Workbook
from importer import load_products
HEADER = ["sku", "name", "quantity", "price", "added"]
def workbook(rows) -> io.BytesIO:
wb = Workbook()
sheet = wb.active
sheet.append(HEADER)
for row in rows:
sheet.append(row)
buffer = io.BytesIO()
wb.save(buffer)
buffer.seek(0)
return buffer
def test_parses_row_and_defaults_optional():
products = load_products(workbook([["A1", "Widget", 3, 9.99, None]]))
assert products[0].sku == "A1"
assert products[0].price == 9.99
assert products[0].added is None
def test_whole_floats_coerce_but_fractions_fail():
assert load_products(workbook([["A1", "Widget", 3.0, 1.0, None]]))[0].quantity == 3
with pytest.raises(ValueError, match="row 2"):
load_products(workbook([["A2", "Widget", 3.5, 1.0, None]]))
def test_strips_whitespace_and_ignores_extra_columns():
product = load_products(workbook([[" A1 ", " Widget ", 1, 1.0, None, "junk"]]))[0]
assert (product.sku, product.name) == ("A1", "Widget")
def test_missing_required_cell_is_null_not_empty_string():
with pytest.raises(ValueError, match="name"):
load_products(workbook([["A1", None, 1, 1.0, None]]))
def test_error_reports_sheet_row_number():
rows = [["A1", "ok", 1, 1.0, None], ["A2", "bad", -1, 1.0, None]]
with pytest.raises(ValueError, match="row 3"):
load_products(workbook(rows))
def test_excel_date_becomes_python_date():
products = load_products(workbook([["A1", "Widget", 1, 1.0, date(2026, 1, 2)]]))
assert products[0].added == date(2026, 1, 2)
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
beartype conf + pydantic model_validate runtime checks
Pair beartype boundary checks with pydantic model_validate, using BeartypeConf to align int/float coercion and route violations, and pin down the gotchas where the two guards disagree.
DeepDiff diffing of pydantic v2 model dumps with ignore_order
Compare pydantic v2 model dumps with DeepDiff, normalizing python vs json dump modes and taming list order, datetime and float gotchas.
Faker and polyfactory seeded Pydantic model factories offline
Define Pydantic models and generate fixtures offline with polyfactory, binding one Faker instance so Use() providers and seed_random stay reproducible.
FastAPI + Pydantic v2 validators, discriminated unions and serializers
Pydantic v2 patterns in FastAPI that agents often get wrong: field_validator, model_validator, discriminated unions, computed_field and response_model filtering.