Python None: Production Patterns for Null Handling at Scale

#Engineering#Architecture#Performance
Python None: Production Patterns for Null Handling at Scale

Python's None is the singleton instance of NoneType, and you should test for it with is None, never == None or a bare truthiness check. In production the hard part isn't the syntax. It's that one value ends up standing for "missing," "unknown," "not provided," "explicitly cleared," and "failed," and those cases cross JSON, SQL, ORMs, and LLM outputs with different semantics at each boundary.

This guide covers the patterns we use to keep None from leaking through service layers: identity checks, sentinels, strict typing, Pydantic PATCH semantics, SQL three-valued logic, and the lint rules that enforce all of it.

Table of Contents

What None Actually Is

None is a language-level singleton. There is exactly one instance per interpreter, which is why identity comparison is both correct and the fastest way to check for it.

>>> type(None)
<class 'NoneType'>
>>> None is None
True
>>> bool(None)
False
>>> def no_return(): pass
>>> no_return() is None
True

Three facts drive most production bugs:

  1. ▹Any function without an explicit return returns None. A forgotten return in a branch silently produces None.
  2. ▹None is falsy. So are 0, "", [], {}, and 0.0. All of those can be legitimate values.
  3. ▹None has almost no behavior. Any attribute access or arithmetic on it raises AttributeError or TypeError, usually far from where the None was introduced.

The official Python data model documentation is the authoritative reference on the singleton's semantics.

The Truthiness Trap

This is the most common None bug we find in audits.

# BUG: discount of 0 is a valid value, but it gets replaced
def apply_discount(price: float, discount: float | None = None) -> float:
    discount = discount or 0.10   # 0.0 -> 0.10
    return price * (1 - discount)

# CORRECT: identity check
def apply_discount(price: float, discount: float | None = None) -> float:
    if discount is None:
        discount = 0.10
    return price * (1 - discount)

The same trap exists in if not items: when None means "not loaded yet" and [] means "loaded, nothing found." Those states need different handling. Cache layers hit this constantly: if None means "cache miss" and the underlying value can also be None, you will hammer your database on every request for a missing record, which is a classic cache-penetration problem. Store a negative-cache marker instead.

_MISSING = object()

def get_user(cache, db, user_id: int):
    hit = cache.get(user_id, _MISSING)
    if hit is not _MISSING:
        return hit            # may legitimately be None (negative cache)
    row = db.fetch_user(user_id)
    cache.set(user_id, row, ttl=60 if row is None else 3600)
    return row

Sentinels: Distinguishing "Not Provided" from None

When None is a valid input meaning "clear this field," you cannot also use it as the default meaning "leave this alone." Use a typed sentinel.

from typing import Final

class _Unset:
    __slots__ = ()
    def __repr__(self) -> str:
        return "UNSET"
    def __bool__(self) -> bool:
        return False

UNSET: Final = _Un
Engineering Consultation

Build With Precision.

Partner with ByteForth to architect autonomous AI agents, enterprise SaaS platforms, and cloud infrastructure. Transparent roadmaps, senior engineering pods, and weekly production releases.

✓

24-Hour Architect Response

Direct technical scoping with senior engineers, zero sales fluff.

✓

100% Client-Owned IP & Strict NDA

Enterprise security standards, clean VPC deployments, and complete code ownership.

✓

US, UK & European Timezone Overlap

Synchronous communication, sprint reviews, and direct Slack integration.