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?
answer
- binary versus decimal fractions
- decimal.Decimal in Python
- total digits and digits after the point
- a 6.1 change to what's required
- SQLite has no real decimal type
basics
~20 sDecimalField 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 linesfrom 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 exactgo deeper
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.
Explain binary float error, the DecimalValidator that runs only during validation, the 6.1 option to omit both arguments, and the SQLite REAL caveat.
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.
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