skip to content

None and Sentinel Values

When 0 and '' are legal inputs, a falsy test throws them away, so you need `is None` or a unique sentinel object. The standard library ships several of its own, and interviewers ask you to name one.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

Why check `if timeout is None:` instead of `if not timeout:` for an optional argument?

level: juniorimportance: must knowfreq 70%

answer

  1. Zero is a real value too
  2. Falsy is a set, not a marker
  3. Ask about absence, not emptiness
  4. None is a singleton - test identity
  5. Empty string and 0 survive `is None`

basics

~20 s

if not timeout: is true for 0, 0.0, '' and [] as well as None, so it silently discards values the caller passed on purpose. is None asks only one question: was the argument omitted?

solid answer

~40 s

Binding an optional parameter to `None` and checking it inside the body is Python's standard way to mean "caller supplied nothing". `if x is None:` tests exactly that: `None` is a singleton, so identity is an exact, unoverridable check. `if not x:` tests something much wider - falsiness - and `0`, `0.0`, `''`, `b''`, `[]`, `{}` and `set()` are all falsy while being perfectly legal arguments. A caller writing `timeout=0` to mean "do not block" or `max_docs=0` to mean "dry run" has that instruction silently replaced by the default, with no error and no log line. Use the falsy test only when empty and absent genuinely mean the same thing for that parameter, and make it a deliberate choice rather than a shorter spelling.

code

python · 7 lines
python
def fetch(timeout=None):
    if timeout is None:
        timeout = 30.0
    return timeout

print(fetch())     # 30.0
print(fetch(0))    # 0  - a falsy test would have replaced it

go deeper

for a junior

Be ready to state the falsy values from memory and to say, in one sentence, why a 0 argument breaks a not x check. Practise writing the default to None, test with is None pattern until it is automatic.

for a middle

Explain the mechanics: not x goes through __bool__ then __len__, while is None is an identity check on a singleton that no class can override. Be able to name a parameter where the falsy test is the correct choice.

for a senior

Show the production angle - this bug takes a wrong branch instead of raising, so it survives tests and reaches operators. Demonstrate the same reasoning on the reading side, where a lookup returning None cannot report whether the key existed.

for a principal

Own it as an API-design rule: decide up front whether a parameter's domain contains falsy values, and whether "absent" and "empty" are distinct states worth spending a sentinel on. Make the answer part of the codebase's conventions rather than a per-function judgement call.

Python has no call-site marker for "this argument was not supplied", so the oldest convention in the language is to bind the parameter to `None` and branch inside the body: ```python def refresh(index, timeout=None): if timeout is None: timeout = 30.0 ``` The defect this question is about appears the moment somebody rewrites that condition as `if not timeout:`. The two look interchangeable and are not. **What each condition actually asks.** `timeout is None` asks a single, narrow question: is this object the one and only instance of `NoneType`? Identity comparison is a pointer comparison; no class can override it, and because `None` is a true singleton for the life of the interpreter it can never accidentally be true for anything else. `not timeout` asks a much broader question: is this object *falsy*? Python answers that by calling `__bool__`, or `__len__` if there is no `__bool__`, and the built-in falsy values include `0`, `0.0`, `0j`, `''`, `b''`, `[]`, `()`, `{}`, `set()`, `range(0)` and `None` itself. Every one of those except `None` is a value a caller may have passed deliberately. **Why it is a silent bug.** Consider a search-index rebuilder with `refresh(index, timeout=None, max_docs=None)`. A caller passes `timeout=0` meaning "never block, fail fast", or `max_docs=0` meaning "walk the plan but write nothing". With a falsy test, both arguments are quietly overwritten by the defaults: the call blocks for thirty seconds and rewrites the whole index. Nothing raises, nothing is logged, and the operator's explicit instruction has vanished. Empty strings behave the same way - an alias of `''` meaning "clear the alias" becomes "no alias supplied, keep the current one". Failures that take the wrong branch rather than raising are exactly the ones that reach production, which is why this shows up as a screening question. **Why `is` rather than `==`.** `x == None` usually works, but equality dispatches to `__eq__`, so a proxy, a mock-like object or a class with a permissive comparison can answer `True`. Identity cannot be faked, and it is what the language itself recommends. **When the falsy test is correct.** Sometimes every falsy value really should take the default path - an optional list of shard names where `[]` and "not supplied" mean the same thing to the function. That is a legitimate design, and `if not shards:` is then the clearer expression of it. The rule is that it must be a decision about the parameter's domain, not a habit. Write down the question every time: *is any legal value for this parameter falsy?* If yes, `is None` is the only safe test. The convention is also safe when the domain contains no falsy member. `re.search()` returns either `None` or a match object, and a match object is always truthy, so `if m:` and `if m is not None:` behave identically there. That is why so much stdlib-flavoured code reads `if m:` without ever being wrong - and why copying that style to a numeric parameter is. **The same trap on the reading side.** `dict.get(key)` returns `None` both when the key is absent and when the key is present holding `None`. If those two states mean different things - "no override configured" versus "override explicitly cleared" - `get` cannot distinguish them at all, and the fix is `key in d`, or `d.get(key, _MISSING)` against a private marker object. The same applies to any lookup, cache or parsed-document field whose values may legitimately be `None`. **Where the convention runs out.** If `None` is itself a legal value for the parameter - a setter where `None` means "clear this field" - then `None` cannot double as the "nothing supplied" marker, and you need a unique sentinel object instead. Standard-library APIs hit this often enough that several ship their own markers rather than reusing `None`. The practical checklist for any optional parameter: default it to `None`; test it with `is None`, never `not x` and never `== None`; and if `None` is a meaningful value in its own right, reach for a dedicated sentinel.

  • When is `if not timeout:` actually the right test?
    When every falsy value should take the same path as an omitted argument - for example an optional list of shard names where an empty list and no list both mean "do them all". The test is whether any legal value for that parameter is falsy. If none is, the two conditions are equivalent and the shorter one is fine; if one is, only `is None` is correct.
  • How do you tell a dictionary key that is absent from one whose value is None?
    `d.get(key)` returns `None` in both cases, so it cannot. Use `key in d` when you only need presence, or `d.get(key, _MISSING)` against a private module-level marker object when you need the value and the presence in one lookup. `dict.setdefault` and `collections.defaultdict` solve a different problem - they fill the missing key rather than reporting it.
  • Why does so much real code get away with `if m:` after `re.search()`?
    Because that function's return domain has no falsy member other than `None` - a match object is always truthy. The convention is safe exactly when no legal value is falsy. The habit becomes a bug when it is carried over to a parameter whose domain includes 0, an empty string or an empty container.

A falsy test is like treating an empty parcel and no parcel at all as the same delivery. The recipient who asked for an empty box never gets it.

saying these in an interview costs you the question

  • Says `if not x:` and `if x is None:` are interchangeable
  • Treats 0 and the empty string as "no value supplied"
  • Writes `x == None` and cannot say why `is` is preferred
  • Uses a magic number such as -1 to mean "not supplied"
  • Assumes `dict.get` distinguishes a missing key from a None value
  • Cannot say what happens when a caller passes 0 deliberately

context

open as a page

In a search-index rebuilder, a module-level `object()` sentinel fails its `is` test inside worker processes - why?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Identity does not survive serialization. Pickling the sentinel to a worker, or deep-copying it, rebuilds a fresh instance, so is compares two different objects. Fix it with a __reduce__ that returns the name, or an enum member.

open as a page

What is Python's `Ellipsis` object, and what is `...` actually used for?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

... is Ellipsis, the single instance of a built-in type with no behaviour of its own. It is truthy and does nothing; code gives it meaning - as a stub body, inside a subscript, or in annotation forms.

open as a page