skip to content

Why should a Django model store a menu item's price in a DecimalField rather than a FloatField, and what do max_digits and decimal_places control?

level: middleimportance: should knowfreq 50%

answer

  1. binary versus decimal fractions
  2. decimal.Decimal in Python
  3. total digits and digits after the point
  4. a 6.1 change to what's required
  5. SQLite has no real decimal type

basics

~20 s

DecimalField maps to a fixed-precision numeric column and Python's decimal.Decimal, so 12.50 stays exact; FloatField uses binary floating point, which cannot represent most prices exactly. max_digits caps total digits and decimal_places the digits after the point.

solid answer

~40 s

`FloatField` stores a binary floating-point number and returns a Python `float`, so a value like `0.1` is only approximated and sums of prices drift. `DecimalField` maps to the database's fixed-precision numeric type and returns `decimal.Decimal`, which is exact for money. `max_digits` is the total number of digits and `decimal_places` how many of them follow the point, so `max_digits=7, decimal_places=2` allows up to 99999.99; `max_digits` must be at least `decimal_places`. Model validation adds a `DecimalValidator` that rejects too many digits, but only when `full_clean()` or a form runs. In Django 6.1 both arguments may be omitted together for an unconstrained numeric column on PostgreSQL, SQLite and Oracle; MySQL and MariaDB still require them. On SQLite, decimals are stored as `REAL`, so exact arithmetic in the database is not available there.

code

python · 4 lines
python
from decimal import Decimal

print(0.1 + 0.2 == 0.3)  # False: binary floats are approximations
print(Decimal("0.1") + Decimal("0.2") == Decimal("0.3"))  # True: decimal arithmetic is exact

go deeper

for a junior

Know that money goes in DecimalField, returned as decimal.Decimal, and that max_digits is total digits while decimal_places counts digits after the point.

for a middle

Explain binary float error, the DecimalValidator that runs only during validation, the 6.1 option to omit both arguments, and the SQLite REAL caveat.

for a senior

Size price columns deliberately, quantize before bulk writes that skip validation, and make sure tests run on the production database type when arithmetic happens in SQL.

for a principal

Decide how the system represents money end to end — decimal types, currency codes, rounding rules — so every service and report agrees to the cent.

## Floats cannot hold most prices exactly A `FloatField` stores an IEEE binary floating-point number and hands back a Python `float`. Binary fractions cannot represent most decimal fractions: `0.1`, `0.2` and `12.30` are all stored as the nearest binary approximation. Individually the error is tiny, but it surfaces in totals, comparisons (`price == 12.3`), rounding at the half-cent and in reconciliations. For money, that is unacceptable. `DecimalField` instead maps to the database's **fixed-precision numeric** type and returns Python's **`decimal.Decimal`**, which represents `12.30` exactly and does decimal arithmetic. | | `FloatField` | `DecimalField` | |---|---|---| | Python type | `float` | `decimal.Decimal` | | column | floating point | fixed-precision numeric | | exact for 12.30? | no | yes | | extra arguments | none | `max_digits`, `decimal_places` | | built-in validator | none | `DecimalValidator` | ## What max_digits and decimal_places mean - **`max_digits`** — the total number of digits, on both sides of the point. - **`decimal_places`** — how many of those digits come after the point. So `DecimalField(max_digits=7, decimal_places=2)` holds values up to `99999.99`, and `max_digits=5, decimal_places=2` holds up to `999.99`. System checks enforce the relationship: `max_digits` must be greater than or equal to `decimal_places` (`fields.E134`), and the two must be given together or omitted together (`fields.E135`). **Django 6.1 change:** until 6.0 both arguments were required on every backend. Since 6.1 you may omit both on PostgreSQL, SQLite and Oracle to get a numeric column with no declared precision; MySQL and MariaDB still require them because they have no unconstrained numeric type. ## Where the limits are enforced 1. **Model validation** — `DecimalField` adds a `DecimalValidator(max_digits, decimal_places)`. It runs in `ModelForm`s, the admin and explicit `full_clean()` calls, and reports too many digits or decimal places as a validation error. 2. **The database column** — the declared precision and scale are part of the column type, so the database has the final say on writes that skip validation, such as `save()` without `full_clean()`, `update()` or `bulk_create()`. How it treats an out-of-range value depends on the database, which is exactly why you validate first. ## Practical rules for a menu price ```python from decimal import Decimal from django.core.validators import MinValueValidator from django.db import models class MenuItem(models.Model): price = models.DecimalField( max_digits=7, decimal_places=2, validators=[MinValueValidator(Decimal("0.01"))], ) discount_rate = models.DecimalField( max_digits=4, decimal_places=3, default=Decimal("0.000") ) ``` - **Use `Decimal` literals**, built from strings: `Decimal("12.50")`, not `Decimal(12.5)` or a bare float. A float passed in is converted, but it was already approximate. - **Size for the future**: a restaurant chain's catering orders may exceed 999.99; widening a column later is a migration. - **Keep the currency elsewhere**: `DecimalField` stores an amount, not a currency; store the currency code in its own field if you ever need more than one. - **Aggregates stay decimal**: `Sum("price")` over a `DecimalField` returns a `Decimal`. ## Rounding explicitly When a price is computed — a discount, a tax, a split bill — round it yourself before saving, rather than relying on validation errors or on what the database does with extra digits: ```python from decimal import ROUND_HALF_UP, Decimal discounted = (item.price * Decimal("0.85")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) ``` `Decimal.quantize()` fixes the number of decimal places and names the rounding rule, so the value matches `decimal_places=2` and the business rule is visible in code. ## The SQLite caveat SQLite has no true decimal storage: Django's documentation notes that decimal values are converted to SQLite's `REAL` (an 8-byte float) internally, so arithmetic done **in the database** is not correctly rounded decimal arithmetic. Values read back into Python are still `Decimal`s, but tests that sum or compare prices in SQL can behave differently on SQLite than on the production database. ## Common mistakes - Choosing `FloatField` because "it's just a number". - Setting `decimal_places=2` but `max_digits=4`, capping prices at 99.99. - Defaulting to `0.0` (a float) instead of `Decimal("0.00")`. - Assuming `DecimalValidator` protects bulk writes; it only runs during validation.

  • Does DecimalField reject 12.345 when decimal_places=2?
    During validation, yes: the field's `DecimalValidator` reports too many decimal places in a `ModelForm`, the admin or `full_clean()`. A plain `save()` does not validate, so the value reaches the database, whose numeric column then decides what happens. Validate or quantize with `Decimal.quantize()` before saving.
  • Can a Django 6.1 DecimalField omit max_digits and decimal_places?
    Yes, both together, on PostgreSQL, SQLite and Oracle, producing a numeric column with no declared precision. Omitting only one is a system-check error (`fields.E135`), and MySQL and MariaDB still require both.

saying these in an interview costs you the question

  • FloatField is exact as long as you round when displaying
  • decimal_places counts the digits before the point
  • max_digits and decimal_places are always required on every backend in Django 6.1
  • DecimalValidator also runs on update() and bulk_create()
  • SQLite stores DecimalField values as exact decimals