Liquids
Every container in the labware model can hold liquid. How you represent that liquid determines how it behaves: whether it carries physical properties that influence pipetting, whether it carries a unique identifier that can be traced back to a sample, and how it homogenizes inside a mixture.
The SDK provides three representations, each with distinct semantics:
- Predefined liquids — common lab reagents accessed via
PredefinedLiquids(water, ethanol, DMSO, etc.) - Custom
Liquidinstances — user-defined reagents with optional physical metadata, created withLiquid("name") Sampleclass — traceable biological specimens with unique identifiers
Predefined Liquids
Predefined liquids are accessed through the PredefinedLiquids registry:
from unitelabs.labware import Liquid, PredefinedLiquids
# Add predefined liquid to a container
container.add_liquid(PredefinedLiquids.WATER, volume=50_000)
container.add_liquid(PredefinedLiquids.ETHANOL, volume=10_000)
Liquid.WATER still works but is deprecated and will be removed in a future version. Use PredefinedLiquids.WATER instead.Available Liquids
| Constant | Display name | Built-in aliases |
|---|---|---|
ACETONITRILE | Acetonitrile | — |
BLOOD | Blood | — |
BRAIN_HOMOGENATE | Brain-Homogenate | Brain Homogenate, brain_homogenate, BrainHomogenate |
CHLOROFORM | Chloroform | — |
DIMETHYL_SULFOXIDE | Dimethyl sulfoxide | DMSO, dimethyl_sulfoxide, DimethylSulfoxide |
ETHANOL | Ethanol | EtOH, ethanol, Ethyl Alcohol, ethyl_alcohol |
GLYCERIN | Glycerin | — |
METHANOL | Methanol | MeOH |
OCTANOL | Octanol | 1-Octanol, octanol |
PBS_BUFFER | PBS Buffer | — |
PLASMA | Plasma | — |
SERUM | Serum | — |
TE_BUFFER | Tris-EDTA buffer | TE Buffer, te_buffer, TEBuffer, Tris EDTA buffer, tris_edta_buffer |
WATER | Water | H2O, h_2_o, H_2_O, water, Aqua |
All name and alias lookups are case-insensitive.
To see the full table at runtime (including any custom aliases you have added):
PredefinedLiquids.show()
Liquid Properties
Liquids can optionally carry properties that influence pipetting behavior:
- Viscosity — viscosity of the liquid in mPa·s
- Density — density of the liquid in g/mL
These properties can be used to select appropriate pipetting parameters (flow rates, mixing speeds, etc.) for optimal liquid transfer.
Custom Liquids
For substances that are not in the predefined list, create a Liquid instance directly. You can optionally supply physical metadata.
Creating a Custom Liquid
from unitelabs.labware import Liquid
# Minimal — name only
tris_buffer = Liquid("Tris buffer")
# With physical metadata
lysis_buffer = Liquid("Lysis buffer", viscosity=1.2, density=1.05)
Liquid with a name that matches a predefined liquid or its aliases raises ValueError. Use the PredefinedLiquids constant instead.# Raises ValueError — "Water" is a predefined liquid
water = Liquid("Water")
# ValueError: Liquid named 'Water' conflicts with predefined liquid 'Water'.
# Use PredefinedLiquids.WATER instead.
# Correct approach
water = PredefinedLiquids.WATER
Equality and Homogenization
Two Liquid instances are equal when their name, viscosity, and density all match. This means the same custom liquid defined in two places will homogenize correctly inside a Mixture:
from unitelabs.labware import Liquid, Mixture
buf_a = Liquid("Lysis buffer", viscosity=1.2, density=1.05)
buf_b = Liquid("Lysis buffer", viscosity=1.2, density=1.05)
mixture = Mixture()
mixture.add(buf_a, 100)
mixture.add(buf_b, 50)
print(len(mixture.ingredients)) # 1 — homogenized into one entry
print(mixture.volume) # 150
Each Liquid instance also carries a short unique identifier (8-character hex string) that is excluded from equality and hashing — it exists solely for tracking purposes:
liq = Liquid("My Buffer")
print(liq.identifier) # e.g. "3f8a1c2b"
Custom Aliases
If your lab uses a different name for a predefined liquid (e.g. your LIMS stores water as "AquaDest"), you can register an alias so that name resolves to the correct predefined instance and homogenizes transparently:
from unitelabs.labware import PredefinedLiquids, Mixture
# Register the alias once at startup
PredefinedLiquids.add_alias(PredefinedLiquids.WATER, "AquaDest")
# From that point on, the alias is interchangeable with the predefined liquid
mixture = Mixture()
mixture.add("AquaDest", 100)
mixture.add("Water", 50)
print(len(mixture.ingredients)) # 1 — both resolve to the same entry
print(mixture.volume) # 150
Rules for Aliases
- An alias can only be added to a predefined liquid; custom
Liquidinstances cannot have aliases. - Aliases must not conflict with any existing predefined name or existing alias (comparison is case-insensitive).
- Multiple aliases for the same liquid are allowed.
# Multiple aliases for the same liquid — OK
PredefinedLiquids.add_alias(PredefinedLiquids.DIMETHYL_SULFOXIDE, "Dimethylsulfoxide")
PredefinedLiquids.add_alias(PredefinedLiquids.DIMETHYL_SULFOXIDE, "Me2SO")
# Raises ValueError — alias conflicts with the predefined name "Water"
PredefinedLiquids.add_alias(PredefinedLiquids.ETHANOL, "Water")
# Raises ValueError — alias targets a custom liquid, not a predefined one
custom = Liquid("My Buffer")
PredefinedLiquids.add_alias(custom, "MB")
Samples
Sample represents traceable biological specimens with unique identifiers:
from unitelabs.labware import Sample
# Create a sample with tracking information
sample = Sample(
name="MySample",
identifier="SAMPLE-001",
)
# Add to container
container.add_liquid(sample, volume=100)
Samples are appropriate when you need:
- Traceability — unique identification and tracking of biological specimens
- Isolation — each sample is distinct, even if the names match
When to Use Each Type
| Type | Use Case | Key Features |
|---|---|---|
| Predefined liquids | Standard reagents (water, ethanol, etc.) | Registry-based, aliases, no construction |
Custom Liquid instances | Workflow-specific buffers and reagents | Optional viscosity/density, homogenization |
Sample class | Traceable biological specimens | Unique IDs, tracking, never homogenized |
Use predefined liquids for standard reagents, custom Liquid instances for workflow-specific reagents, and Sample for traceable biological specimens.
Pre-filling Labware
Adding liquid one container at a time is useful in many cases as volumes or liquids vary between the wells and granural control is needed.
plate = Standard96Plate(identifier="plate")
for well in plate:
well.container.add_liquid(PredefinedLiquids.WATER, 100)
If a whole plate is filled with same volume of a liquid, a simpler method on labware construction can be preferable.
Pipettable labware — plates and troughs — accept a filled_with argument that does this at construction, mirroring the way a TipRack is pre-filled with tips:
from unitelabs.labware import PredefinedLiquids
from unitelabs.labware.plates import Standard96Plate
plate = Standard96Plate(identifier="plate", filled_with=(PredefinedLiquids.WATER, 100))
filled_with takes a single (liquid, volume) pair, or a list of pairs to pre-fill each container with a mixture. The volume is the amount in microliters added to every container of the labware:
plate = Standard96Plate(
identifier="plate",
filled_with=[(PredefinedLiquids.WATER, 80), (PredefinedLiquids.ETHANOL, 20)],
)
"Every container" is what keeps the behavior uniform across labware shapes — the argument fills whatever the labware's containers are, whether that is 96 wells on a plate, the twelve independent channels of a column trough, or the single reservoir of a simple trough:
from unitelabs.labware.troughs import StandardTrough12Column
# Each of the twelve columns is filled with the same mixture
reservoir = StandardTrough12Column(
identifier="reservoir",
filled_with=[(PredefinedLiquids.WATER, 80), (PredefinedLiquids.ETHANOL, 20)],
)
filled_with volume that exceeds a container's max_volume fails immediately at construction with LiquidOverflowError, rather than silently overfilling.To fill only some containers, leave filled_with off and add liquid to the wells you want — slicing works with chess-key ranges:
plate = Standard96Plate(identifier="plate")
for well in plate["A1":"H1"]:
well.container.add_liquid(PredefinedLiquids.WATER, 100)
Migration Guide
The Liquid type was redesigned from a str enum into a frozen dataclass. Predefined substances are no longer members of the Liquid class itself; they live in the PredefinedLiquids registry. Custom liquids are now plain Liquid instances.
Import changes
# Before
from unitelabs.labware import Liquid
# After
from unitelabs.labware import Liquid, PredefinedLiquids
Accessing predefined liquids
# Before — Liquid was a str(enum.Enum)
container.add_liquid(Liquid.WATER, volume=50_000)
container.add_liquid(Liquid.DMSO, volume=10_000)
# After — predefined liquids live in PredefinedLiquids
container.add_liquid(PredefinedLiquids.WATER, volume=50_000)
container.add_liquid(PredefinedLiquids.DIMETHYL_SULFOXIDE, volume=10_000)
Liquid.WATER is deprecated and will be removed in a future version — use PredefinedLiquids.WATER instead.
String comparison no longer works directly
# Before — Liquid was a str enum, so this was True
Liquid.WATER == "Water" # True
# After — Liquid is a frozen dataclass
PredefinedLiquids.WATER == "Water" # False
str(PredefinedLiquids.WATER) == "Water" # True — compare via str() if needed
Constructing a Liquid with a predefined name is now forbidden
# Before — silently created a duplicate
water = Liquid("Water")
# After — raises ValueError
water = Liquid("Water")
# ValueError: Liquid named 'Water' conflicts with predefined liquid 'Water'.
# Use PredefinedLiquids.WATER instead.
The same guard applies to all built-in aliases ("H2O", "DMSO", "EtOH", etc.) and is case-insensitive.
PredefinedLiquids API Reference
from unitelabs.labware import PredefinedLiquids
| Method / attribute | Description |
|---|---|
PredefinedLiquids.WATER | Direct attribute access to any predefined liquid |
PredefinedLiquids.get_by_name(name) | Case-insensitive lookup by name or alias; returns Liquid or None |
PredefinedLiquids.get_by_identifier(id) | Lookup by the identifier field; returns Liquid or None |
PredefinedLiquids.add_alias(liquid, alias) | Register a custom alias for a predefined liquid |
PredefinedLiquids.find(**kwargs) | Filter predefined liquids by name, viscosity, and/or density |
PredefinedLiquids.show() | Print a formatted table of all predefined liquids, aliases, and properties |
Integration with Liquid Classes
Custom liquids with viscosity and density properties can influence liquid-class selection for pipetting parameters. See Liquid Classes.