Plates — visual walkthrough

Plates are managed tables specialised for microplates. You collect samples in a flat plate table, project them onto a plate layout (a rows × columns well grid that mirrors the bench), then round-trip the grid back to a tidy DataFrame. Three functions tie it together: create_plate_table, create_plate_layout, and plate_layout_to_dataframe — with formats from get_plate_format_presets.

Title / header Data cell Plate Position dropdown Variable line in a well Inserted on grow Full-row delete on shrink Stacked plate below

1. Plate table create_plate_table

create_plate_table writes a data-entry sheet (default "plate sheet"): a merged title band, an orange header row of Sample No. / Sample ID / Plate Position plus any extra_headers, and a bordered data area of sample_rows rows (defaulting to the format's well count). Each header cell gets a sheet-scoped named range (sample_id, plate_position, concentration, …) and the whole row is named plate_table.

"plate sheet" after create_plate_table(wb, plate_format="96", extra_headers=["Batch","Concentration"])
B
C
D
E
F
3
Plate Table
4
Sample No.
Sample ID
Plate Position
Batch
Concentration
5
1
A1 ▾
6
2
B1 ▾
7
3
C1 ▾
… 96 rows: Plate Position pre-filled column-major A1, B1, …, H1, A2, …, H12 …

Plate Position is a validated dropdown. Every cell in the Plate Position column carries an in-cell Data Validation list (xlValidateList, xlValidAlertStop) of every valid well for the chosen format (A1 … H12 for 96-well). The options are written to a separate list sheet (default "lists") via add_dropdown_list, which keeps positions canonical so the layout can match on them.

Row 1 holds hidden metadata — plate_format_key (A1), plate_rows_count_key (B1), plate_cols_count_key (C1). create_plate_layout reads these to detect the format automatically, so don't delete them. start_col defaults to 2 (column B), leaving column A free. Writes escape leading = + - @ by default (sanitize_formulas=True).

2. Table → plate layout create_plate_layout

Fill the table (type or paste Sample ID, pick Plate Position from the dropdown, add per-well variables), then call create_plate_layout. It fills wells by matching Plate Position values — well A1 gets the row whose plate_position == "A1", not by row order. The variables in value_named_ranges stack vertically inside each well in well_value_order.

Before — filled "plate sheet" (24-well source)
Sample ID
Plate Pos.
Conc.
5
S-001
A1
0.1
6
S-002
B1
0.2
7
S-003
A2
0.3
8
S-004
B2
0.4
… through C6 / D6 (24 wells) …
After — "plate layouts" · 24-well (A–D × 1–6)
1
2
3
4
5
6
Vars
A
S-001
0.1
S-003
0.3
S-005
0.5
S-007
0.7
S-009
0.9
S-011
1.1
Sample ID
Conc.
B
S-002
0.2
S-004
0.4
S-006
0.6
S-008
0.8
S-010
1.0
S-012
1.2
C
S-013
1.3
S-015
1.5
S-017
1.7
S-019
1.9
S-021
2.1
S-023
2.3
D
S-014
1.4
S-016
1.6
S-018
1.8
S-020
2.0
S-022
2.2
S-024
2.4

The format is read automatically from the source sheet's row-1 metadata — you don't pass it again. Each well stacks the chosen variables in well_value_order (here sample_id then concentration), and a Variables label column names the lines on the right. Duplicate positions, or a well with no matching row, raise ValueError.

Example:

ws = create_plate_layout(
    wb,
    plate_name="plate_run_001",
    plate_title="Run 001 — Batch A",
    value_named_ranges=["sample_id", "concentration"],
    well_value_order=["sample_id", "concentration"],
    source_sheet_name="plate sheet",
    target_sheet_name="plate layouts",
)

3. Supported formats get_plate_format_presets

get_plate_format_presets() returns the built-in formats keyed by well count. The layout grows in the same shape — just bigger.

"6" · 6-well · 2 × 3 · A–B "12" · 12-well · 3 × 4 · A–C "24" · 24-well · 4 × 6 · A–D "48" · 48-well · 6 × 8 · A–F "96" · 96-well · 8 × 12 · A–H default "384" · 384-well · 16 × 24 · A–P "1536" · 1536-well · 32 × 48 · A–AF

Custom dimensions. When no preset fits, pass both plate_rows_count and plate_cols_count (only one raises ValueError); the shape stores as "custom". Row labels extend past 26 the same way Excel columns do — A–Z, then AA–ZZ, then AAA–ZZZ (up to 18,278 rows), so a 1536-well plate's rows spill into AA … AF and tall custom plates round-trip correctly.

96-well grid (A–H × 1–12), shown compactly

1
2
3
4
5
6
7
8
9
10
11
12
A
B
C
D
E
F
G
H
384-well = A–P × 1–24 · 1536-well = A–AF × 1–48 — same shape, larger

4. Round-trip read-back plate_layout_to_dataframe

plate_layout_to_dataframe reads a drawn plate grid back into a tidy DataFrame using the named ranges create_plate_layout wrote — the format is inferred automatically, no format parameter. Every frame includes plate_name, plate_row, plate_col, and plate_position; that plate_position column round-trips exactly into a new plate table's Plate Position column.

Plate grid (24-well, "plate layouts")
1
2
3
4
5
6
A
S-001
0.1
S-003
0.3
S-005
0.5
S-007
0.7
S-009
0.9
S-011
1.1
B
S-002
0.2
S-004
0.4
S-006
0.6
S-008
0.8
S-010
1.0
S-012
1.2
… rows C, D …
DataFrame (return_format="wide")
plate_row
plate_col
plate_position
Sample ID
Conc.
0
A
1
A1
S-001
0.1
1
B
1
B1
S-002
0.2
2
A
2
A2
S-003
0.3
3
B
2
B2
S-004
0.4
… one row per well (24) …

Filter with variables (by display name or token), rows (["A","B"]), and columns ([1,2,3]), or switch to return_format="long" (one row per well × variable, with variable_token + variable_display). Row labels are sorted by physical Excel row position, so a 40-row custom plate (A … AN) returns in true plate order, not alphabetically.

Re-running & stacked plates danger zone

Re-call create_plate_layout with the same plate_name to refresh a plate in place. It clears the old footprint and redraws — including a format change. Growing taller (96 → 384) inserts rows so plates stacked below are pushed down, not overwritten. Shrinking (384 → 96) removes the excess rows with a full-row delete so plates below shift up and stay compact.

Shrink — full-row delete band
1
2
3
4
5
6
A
B
these whole worksheet rows are DELETED on shrink ↓
del
del
del
del
del
del
plates below shift UP into this space
plate 2
plate 2
plate 2
plate 2
plate 2
plate 2
What survives vs. what's lost
B–G (plate)
.
.
I (loose)
.
.
x
stacked plate below — content preserved, formatting rebuilt
loose note / chart beside the plate — REMOVED

Danger zone — full-row delete on shrink. Shrinking a plate deletes entire worksheet rows across the shrink band. This deliberately protects plates stacked directly below (their cell content is preserved; merged formatting is rebuilt). But any loose, side-by-side content in other columns on those same rows — notes, a chart, an unmanaged table beside the plate — sits in the delete path and will be removed. Keep the columns beside a plate clear of anything you can't lose, or stack content vertically.

Before any insert/delete the stacked-only invariant is enforced: if another managed table's projected row range would overlap this plate's footprint, you get SideBySideNotSupportedError and nothing is written. A wide format at a high start_col that runs past Excel's last column (XFD = 16,384) is rejected before writing with ExcelColumnOverflowError.