Configuring and Working with PintFields¶
Choosing Between IntegerPintField and DecimalPintField¶
IntegerPintField stores quantities as whole numbers. Use it for counts, simple measurements, or anything where fractional values are irrelevant.
DecimalPintField stores quantities with full decimal precision. Use it for scientific measurements, financial calculations, or any situation where you need fractional values. It supports display_decimal_places and rounding_method parameters for fine-grained control over how values are displayed and rounded.
Both field types store the comparator (used for cross-unit database comparisons) as a Decimal internally, so comparisons remain accurate regardless of which field type you choose.
Configuring unit_choices¶
The unit_choices parameter controls which units appear in forms and widgets. You can specify it in several formats:
# Simple string list
unit_choices=["gram", "kilogram", "pound"]
# With display names
unit_choices=[
("Gram", "gram"),
("Kilogram", "kilogram"),
("Pound", "pound"),
]
# Mixed format
unit_choices=[
"gram",
("Kilogram", "kilogram"),
["Pound", "pound"],
]
The default_unit is always added internally even if you do not include it in unit_choices. All units listed in unit_choices must share the same dimensionality as the default_unit.
Defining and Using Custom Units¶
Pint lets you define arbitrary units through its UnitRegistry. This is useful for domain-specific quantities like servings, scoops, or any measurement not covered by standard SI or imperial units.
Creating Custom Unit Definitions¶
Create a new registry and define your units. This belongs in a dedicated configuration file or in your Django settings module.
from decimal import Decimal
from pint import UnitRegistry
# Create a new registry with Decimal support
custom_ureg = UnitRegistry(non_int_type=Decimal)
# Define basic custom units
custom_ureg.define("serving = [serving]") # Base unit for serving sizes
custom_ureg.define("scoop = 2 * serving") # Define a scoop as 2 servings
custom_ureg.define("portion = 4 * serving") # Define a portion as 4 servings
# Define derived units
custom_ureg.define("calorie_density = calorie / gram") # Energy density
custom_ureg.define("price_per_kg = dollar / kilogram") # Price density
# Define prefixed units
custom_ureg.define("halfserving = 0.5 * serving")
custom_ureg.define("doubleserving = 2 * serving")
For the full syntax and capabilities, see the Pint documentation on defining units.
Registering Custom Units¶
Tell Django Pint Field to use your custom registry by setting it in settings.py:
# settings.py
# Set the custom registry as the default for the project
DJANGO_PINT_FIELD_UNIT_REGISTER = custom_ureg
Using Custom Units in Models¶
Once registered, custom units work the same as built-in ones:
from django.db import models
from django_pint_field.models import DecimalPintField
class Recipe(models.Model):
name = models.CharField(max_length=100)
serving_size = DecimalPintField(
"serving", # Using our custom base unit
display_decimal_places=2,
unit_choices=[
"serving",
"scoop",
"portion",
"halfserving",
],
)
energy_density = DecimalPintField(
"calorie_density", # Using our custom derived unit
display_decimal_places=2,
)
def __str__(self):
return self.name
Controlling Decimal Precision and Rounding¶
Precision Management¶
Control decimal precision at the global level and per field:
# Global precision setting in settings.py
DJANGO_PINT_FIELD_DECIMAL_PRECISION = 28 # Set project-wide precision
# Model-level precision control
class PreciseWeight(models.Model):
# Field with specific precision requirements
weight = DecimalPintField(
"gram",
display_decimal_places=4, # Decimal places to display by default
unit_choices=["gram", "kilogram", "pound"],
)
class Meta:
constraints = [
models.CheckConstraint(
check=models.Q(weight__isnull=False),
name="weight_not_null",
)
]
Rounding Strategies¶
The rounding_method parameter on DecimalPintField accepts any of the standard decimal module rounding modes:
ROUND_HALF_UP(default)ROUND_DOWNROUND_CEILINGROUND_FLOORROUND_HALF_DOWNROUND_HALF_EVENROUND_UPROUND_05UP
from decimal import Decimal, ROUND_HALF_UP, ROUND_DOWN, ROUND_CEILING
from django_pint_field.units import ureg
class WeightMeasurement(models.Model):
weight = DecimalPintField(
"gram",
display_decimal_places=2,
rounding_method=ROUND_HALF_UP, # Default rounding method
)
def round_weight(self, method=ROUND_HALF_UP, places=2):
"""Round weight using specified method and decimal places"""
if self.weight:
magnitude = Decimal(str(self.weight.magnitude))
rounded = magnitude.quantize(Decimal("0.01"), rounding=method)
return ureg.Quantity(rounded, self.weight.units)
return None
def get_rounded_weights(self):
"""Get weight rounded using different methods"""
return {
"half_up": self.round_weight(ROUND_HALF_UP),
"down": self.round_weight(ROUND_DOWN),
"ceiling": self.round_weight(ROUND_CEILING),
}
Decimal Context Configuration¶
For calculations that need temporary higher precision, use Python’s localcontext:
from decimal import getcontext, localcontext
from django_pint_field.units import ureg
class HighPrecisionMeasurement(models.Model):
value = DecimalPintField(
"meter",
display_decimal_places=10,
)
def precise_calculation(self):
"""Perform high-precision calculations"""
with localcontext() as ctx:
# Temporarily set higher precision for this calculation
ctx.prec = 50
if self.value:
# Perform precise calculation
result = (self.value**2).magnitude
# Return with original precision
return Decimal(str(result)).quantize(Decimal("0.0000000001"))
@staticmethod
def set_global_precision(precision):
"""Set global decimal precision"""
getcontext().prec = precision
Working with Derived Units¶
Derived units combine multiple base units. They are useful for quantities like velocity, acceleration, density, and similar computed measurements.
from django.db import models
from django_pint_field.models import DecimalPintField
from django_pint_field.units import ureg
class PhysicalMeasurement(models.Model):
# Basic measurements
length = DecimalPintField(
"meter",
display_decimal_places=2,
)
time = DecimalPintField(
"second",
display_decimal_places=2,
)
# Derived measurements
velocity = DecimalPintField(
"meter/second",
display_decimal_places=2,
)
acceleration = DecimalPintField(
"meter/second**2",
display_decimal_places=2,
)
def calculate_velocity(self):
"""Calculate velocity from length and time"""
if self.length and self.time:
try:
self.velocity = self.length / self.time
return self.velocity
except Exception as e:
raise ValueError(f"Velocity calculation error: {e}")
def calculate_acceleration(self):
"""Calculate acceleration from velocity and time"""
if self.velocity and self.time:
try:
self.acceleration = self.velocity / self.time
return self.acceleration
except Exception as e:
raise ValueError(f"Acceleration calculation error: {e}")
Validating PintField Values¶
Built-in Validation¶
PintFields automatically validate dimensionality. If you define a field with default_unit="gram", assigning a length quantity (like meters) raises a ValidationError. When using DecimalPintField, decimal validation follows the active Decimal context precision; display_decimal_places controls rendering, not storage precision.
Custom Validation in clean()¶
Add range checks and other constraints in your model’s clean() method:
from django.core.exceptions import ValidationError
from django.db import models
from django_pint_field.models import DecimalPintField
from django_pint_field.units import ureg
class ValidatedMeasurement(models.Model):
value = DecimalPintField(
"meter",
display_decimal_places=2,
unit_choices=["meter", "kilometer", "mile"],
)
def clean(self) -> None:
"""Validate measurement"""
if self.value is not None:
# Validate range
if self.value.quantity.to("meter").magnitude > 1000000:
raise ValidationError("Value cannot exceed 1,000,000 meters")
def save(self, *args, **kwargs) -> None:
"""Save with validation"""
self.clean()
super().save(*args, **kwargs)
@classmethod
def get_valid_units(cls) -> list:
"""Get list of valid units"""
field = cls._meta.get_field("value")
return field.unit_choices
Range Validation with Decimal Precision¶
For tighter control, use Decimal values in your range checks:
from decimal import Decimal
from django_pint_field.units import ureg
class Product(models.Model):
weight = DecimalPintField(
"gram",
display_decimal_places=2,
unit_choices=["gram", "kilogram"],
)
def clean(self):
super().clean()
if self.weight:
# Dimensionality validation is automatic;
# assigning a length unit to a weight field raises ValidationError
# Custom range validation with Decimal precision
min_weight = Decimal("0.1") * ureg.kilogram
max_weight = Decimal("100.0") * ureg.kilogram
if self.weight < min_weight:
raise ValidationError(
{"weight": f"Weight must be at least {min_weight:.2f}"}
)
if self.weight > max_weight:
raise ValidationError(
{"weight": f"Weight cannot exceed {max_weight:.2f}"}
)
# Decimal precision validation follows the active Decimal context;
# display_decimal_places only affects rendering
Building Reusable Conversion Utilities¶
A mixin keeps conversion logic in one place and avoids repeating the same patterns across models:
from typing import Dict, Any, Optional
from decimal import Decimal
from django_pint_field.units import ureg
class ConversionMixin:
"""Mixin for common conversion patterns"""
def to_unit(self, unit: str, round_digits: int = 2) -> ureg.Quantity:
"""Convert to specified unit with rounding"""
if self.value is None:
return None
converted = self.value.quantity.to(unit)
magnitude = Decimal(str(converted.magnitude))
rounded = round(magnitude, round_digits)
return ureg.Quantity(rounded, unit)
def get_all_units(self) -> Dict[str, Any]:
"""Get value in all available units"""
if self.value is None:
return {}
return {unit_value: self.to_unit(unit_value) for _label, unit_value in self.get_valid_units()}
def format_value(self, unit: Optional[str] = None, decimal_places: int = 2) -> str:
"""Format value for display"""
if self.value is None:
return ""
value = self.to_unit(unit) if unit else self.value.quantity
return f"{value.magnitude:.{decimal_places}f} {value.units}"
class EnhancedMeasurement(ConversionMixin, ValidatedMeasurement):
class Meta:
proxy = True
Use EnhancedMeasurement anywhere you need conversion and validation together:
m = EnhancedMeasurement.objects.first()
m.to_unit("kilometer") # Quantity with rounded magnitude
m.get_all_units() # Dict of all unit_choices conversions
m.format_value("mile", 4) # "0.6214 mile"
Handling Conversion Errors Safely¶
Define specific exception types and a handler class to keep error recovery consistent across your project:
from django.core.exceptions import ValidationError
from django_pint_field.models import DecimalPintField
from django_pint_field.units import ureg
from decimal import Decimal
from typing import Optional
import logging
logger = logging.getLogger(__name__)
class MeasurementError(Exception):
"""Base exception for measurement errors"""
class UnitConversionError(MeasurementError):
"""Error for unit conversion issues"""
class MeasurementHandler:
"""Handler for safe measurement operations"""
@staticmethod
def safe_convert(value, target_unit: str) -> Optional[ureg.Quantity]:
"""Safely convert between units"""
try:
quantity = value.quantity if hasattr(value, "quantity") else value
return quantity.to(target_unit)
except Exception as e:
raise UnitConversionError(
f"Could not convert {value} to {target_unit}: {str(e)}"
)
@staticmethod
def safe_create(value: float | int | Decimal, unit: str) -> ureg.Quantity:
"""Safely create a quantity"""
try:
return ureg.Quantity(value, unit)
except Exception as e:
raise MeasurementError(
f"Could not create quantity with {value} {unit}: {str(e)}"
)
class SafeMeasurement(models.Model):
value = DecimalPintField("meter", display_decimal_places=2)
def convert_value(self, target_unit: str) -> Optional[ureg.Quantity]:
"""Safely convert measurement value"""
if self.value is None:
return None
try:
return MeasurementHandler.safe_convert(self.value.quantity, target_unit)
except UnitConversionError as e:
logger.error(f"Conversion error: {e}")
return None
This pattern returns None on failure instead of crashing, while still logging the underlying error for debugging.
Handling Edge Cases¶
Very Large and Very Small Quantities¶
When magnitudes span many orders of magnitude, scientific notation and automatic prefix selection help keep output readable.
Python’s default Decimal context provides 28 significant digits. Values whose base-unit comparator exceeds that range (for example, converting nanometers to meters multiplies by 10^-9, consuming 9 digits of headroom) can lose precision silently. If your application mixes very large and very small quantities, raise the precision with DJANGO_PINT_FIELD_DECIMAL_PRECISION in your settings. See Configuration for details.
from decimal import Decimal
from django_pint_field.models import DecimalPintField
from typing import Optional
class ExtremeMeasurement(models.Model):
"""Model for handling extreme values"""
value = DecimalPintField(
default_unit="meter",
display_decimal_places=15,
)
def format_scientific(self, decimal_places: int = 2) -> str:
"""Format in scientific notation"""
if self.value is None:
return ""
magnitude = self.value.magnitude
if isinstance(magnitude, Decimal):
return f"{magnitude:.{decimal_places}E} {self.value.units}"
return str(self.value)
def get_human_readable(self) -> str:
"""Get human-readable format with appropriate unit prefix"""
if self.value is None:
return ""
prefixes = [
("femto", 1e-15),
("pico", 1e-12),
("nano", 1e-9),
("micro", 1e-6),
("milli", 1e-3),
("", 1),
("kilo", 1e3),
("mega", 1e6),
("giga", 1e9),
("tera", 1e12),
("peta", 1e15),
]
magnitude = float(self.value.magnitude)
base_unit = str(self.value.units)
for prefix, scale in reversed(prefixes):
if abs(magnitude) >= scale:
converted = magnitude / scale
return f"{converted:.2f} {prefix}{base_unit}"
return str(self.value)
Zero and Negative Values¶
When your domain requires non-negative or non-zero quantities, validate in clean():
from django.core.exceptions import ValidationError
class SignAwareMeasurement(models.Model):
"""Model handling zero and negative values"""
value = DecimalPintField(
"meter",
display_decimal_places=2,
)
def clean(self) -> None:
"""Validate sign-specific constraints"""
super().clean()
if self.value is not None:
self.validate_sign(self.value.magnitude)
@staticmethod
def validate_sign(value: Decimal) -> None:
"""Validate value sign"""
if value < 0:
raise ValidationError("Negative values not allowed")
if value == 0:
raise ValidationError("Zero values not allowed")
def get_absolute(self) -> Optional[ureg.Quantity]:
"""Get absolute value"""
if self.value is None:
return None
magnitude = abs(self.value.magnitude)
return ureg.Quantity(magnitude, self.value.units)
def get_sign(self) -> Optional[int]:
"""Get value sign: 1, -1, or 0"""
if self.value is None:
return None
if self.value.magnitude > 0:
return 1
elif self.value.magnitude < 0:
return -1
return 0
Converting Between Unit Systems¶
Store values in both metric and imperial, and convert freely between them:
from decimal import Decimal
from django_pint_field.units import ureg
class UnitSystemConverter(models.Model):
metric_value = DecimalPintField(
"meter",
display_decimal_places=2,
unit_choices=[
"millimeter",
"centimeter",
"meter",
"kilometer",
],
)
imperial_value = DecimalPintField(
"inch",
display_decimal_places=2,
unit_choices=["inch", "foot", "yard", "mile"],
)
def convert_to_imperial(self):
"""Convert metric value to imperial"""
if self.metric_value:
self.imperial_value = self.metric_value.quantity.to("inch")
def convert_to_metric(self):
"""Convert imperial value to metric"""
if self.imperial_value:
self.metric_value = self.imperial_value.quantity.to("meter")
def get_all_conversions(self):
"""Get value in all available units"""
if self.metric_value:
base_value = self.metric_value.quantity
elif self.imperial_value:
base_value = self.imperial_value.quantity
else:
return None
return {
"metric": {
"mm": base_value.to("millimeter").magnitude,
"cm": base_value.to("centimeter").magnitude,
"m": base_value.to("meter").magnitude,
"km": base_value.to("kilometer").magnitude,
},
"imperial": {
"in": base_value.to("inch").magnitude,
"ft": base_value.to("foot").magnitude,
"yd": base_value.to("yard").magnitude,
"mi": base_value.to("mile").magnitude,
},
}
def round_conversion(self, value, decimals=2):
"""Round converted values to specified decimal places"""
return (
Decimal(str(value.magnitude)).quantize(Decimal(10) ** -decimals)
* value.units
)
Usage:
converter = UnitSystemConverter(metric_value=ureg.Quantity("5.0 * kilometer"))
converter.convert_to_imperial()
print(converter.imperial_value) # 196850.39... inch
conversions = converter.get_all_conversions()
# conversions["metric"]["km"] -> 5.0
# conversions["imperial"]["mi"] -> 3.1068...
Cross-References¶
See API Reference for the full list of field parameters
See Cheatsheet for quick syntax examples