DEV Community

Cover image for null=True on CharField Is Always Wrong — Here Is Why
Houssem Reggai
Houssem Reggai

Posted on

null=True on CharField Is Always Wrong — Here Is Why

PROFESSIONAL DJANGO ENGINEERING SERIES #4

Two ways to represent 'no value' means every query that checks for empty strings has to handle both. The rule is simple once you know it.

Django provides two separate flags for handling empty values on model fields. They are frequently confused, and conflating them leads to subtle bugs, inconsistent data, and queries that are harder to write than they should be.

Here is the rule stated plainly, and then the reasoning behind it.

null=True is a database directive. blank=True is a validation directive. They are independent. Think about each one separately for every field you define.

The Rule

# String-based fields (CharField, TextField, EmailField, SlugField, URLField)
# ─────────────────────────────────────────────────────────────────────
# Required field:
name = models.CharField(max_length=100)

# Optional field — use blank=True ONLY. Never null=True.
nickname = models.CharField(max_length=100, blank=True)

# WRONG: creates two possible 'empty' values for a string field
nickname = models.CharField(max_length=100, null=True, blank=True)  # BAD

# Non-string fields (IntegerField, DateField, ForeignKey, DecimalField)
# ─────────────────────────────────────────────────────────────────────
# Optional non-string: use null=True. Add blank=True if used in forms.
date_of_birth = models.DateField(null=True, blank=True)
manager = models.ForeignKey(
    settings.AUTH_USER_MODEL,
    null=True, blank=True,
    on_delete=models.SET_NULL,
)

# BooleanField: never null=True unless you genuinely need three states
is_active   = models.BooleanField(default=True)   # correct
is_verified = models.BooleanField(default=False)  # correct
Enter fullscreen mode Exit fullscreen mode

Why null=True on CharField Causes Problems

When you set null=True on a string field, your database can now store two different representations of ‘no value’: the empty string '' and NULL. This forces every query that checks for an empty value to handle both cases:

# With null=True on CharField, you need this everywhere:
User.objects.filter(Q(nickname='') | Q(nickname__isnull=True))

# Without null=True, this simple query is correct and complete:
User.objects.filter(nickname='')

# Or using the cleaner Django convention for 'has a nickname':
User.objects.exclude(nickname='')
Enter fullscreen mode Exit fullscreen mode

Django’s own convention for optional string fields is to store '' for ‘no value’, giving you a single canonical empty representation. null=True creates ambiguity that has to be accounted for in every query, every form, every comparison, and every piece of code that displays or validates the field.

The Full Decision Table

# FIELD TYPE                | REQUIRED           | OPTIONAL
# ─────────────────────────────────────────────────────────────
# CharField / TextField     | (no flags)         | blank=True
# EmailField / SlugField    | (no flags)         | blank=True
# IntegerField              | (no flags)         | null=True, blank=True
# DecimalField              | (no flags)         | null=True, blank=True
# DateField / DateTimeField | (no flags)         | null=True, blank=True
# ForeignKey                | (no flags)         | null=True, blank=True
# OneToOneField             | (no flags)         | null=True, blank=True
# BooleanField              | default=True/False | (no flags, use default)
# JSONField                 | default=dict/list  | blank=True (or null=True)
# ImageField / FileField    | (no flags)         | blank=True
Enter fullscreen mode Exit fullscreen mode

Two More Field Type Mistakes Worth Noting

Never use FloatField for money

Floating point arithmetic is imprecise. 0.1 + 0.2 in Python is not 0.3. For monetary values, always use DecimalField(max_digits=10, decimal_places=2). Or store integer cents: price_cents = models.PositiveIntegerField(). Never use FloatField or CharField for money.

Use TextChoices instead of integer status codes

# BAD: magic integers with no context
class Order(models.Model):
    STATUS_PENDING = 1
    status = models.IntegerField(default=1)

# GOOD: self-documenting, validated, admin-friendly
class Order(models.Model):
    class Status(models.TextChoices):
        PENDING  = 'pending',  'Pending'
        PAID     = 'paid',     'Paid'
        SHIPPED  = 'shipped',  'Shipped'

    status = models.CharField(
        max_length=20,
        choices=Status.choices,
        default=Status.PENDING,
    )

# Your database rows are now self-documenting.
# A raw SQL query shows 'paid', not '2'.
# Invalid values are rejected at the model level.
Enter fullscreen mode Exit fullscreen mode

Field design decisions are among the most expensive to change once a table has data in it. Getting them right at the start — following the null/blank rule, using DecimalField for money, using TextChoices for enumerables — is one of the highest-leverage habits a Django developer can build.

Top comments (0)