How do you detect an omitted argument when None itself is a legal value?
answer
- None can be a real value too
- You need a marker no caller owns
- One unique object, compared by identity
- Module level, evaluated once at def time
- dataclasses and inspect ship their own
basics
~20 sCreate one private marker at module level with _MISSING = object(), use it as the parameter's default, and test arg is _MISSING. Nothing else in the program is that object, so None becomes an ordinary value again.
solid answer
~50 sWhen `None` is a meaningful argument - "clear this field" as opposed to "leave it alone" - it can no longer double as the not-supplied marker, so you introduce a sentinel: a single unique object bound at module level, conventionally `_MISSING = object()`, used as the default and compared with `is`. `object()` is the right choice because it is cheap, unique, and compares equal to nothing but itself, so no caller can accidentally produce one. Give it a leading underscore to signal that it is not part of the public API, or wrap it in a tiny private class with a `__repr__` so tracebacks and rendered signatures read sensibly. The standard library does exactly this where `None` is taken: `dataclasses.MISSING` marks a field with no default, `inspect.Parameter.empty` marks a parameter with no default, and `argparse.SUPPRESS` marks an argument that should not appear in the namespace.
code
python · 10 lines_MISSING = object()
def update(config, ttl=_MISSING):
if ttl is _MISSING:
return "ttl untouched"
return f"ttl set to {ttl!r}"
print(update({})) # ttl untouched
print(update({}, None)) # ttl set to None
print(update({}, 0)) # ttl set to 0go deeper
Recall the shape: one module-level _MISSING = object(), used as the default, tested with is. Be able to say why None cannot always play that role - because sometimes None is the value the caller meant.
Explain the mechanics: defaults are evaluated once at def time, object() is unique and equal only to itself, and the underscore marks it private. Name at least one standard-library equivalent such as dataclasses.MISSING or inspect.Parameter.empty.
Demonstrate the API judgement - which parameters genuinely have three states, when **kwargs presence is the better answer, and what it costs to publish a sentinel as part of the public surface. Give it a readable __repr__ because you have read the tracebacks.
Own the consistency question: one sentinel convention per codebase, documented, rather than a different marker per module. Decide whether sentinels appear in the public API at all, and what that commits you to when values start crossing process or serialization boundaries.
The `None`-as-default convention works until `None` becomes a value in its own right. A shard-configuration updater is the standard example: `update(name, alias=None)` cannot express both "leave the alias as it is" and "clear the alias", because both spell themselves `None`. Any API with three states - set it, clear it, do not touch it - needs a marker that no caller can produce by accident. **The pattern.** Create one unique object at import time and use it as the default: ```python _MISSING = object() def update(name, alias=_MISSING, replicas=_MISSING): if alias is not _MISSING: ... # caller asked for a change, possibly to None ``` The default expression is evaluated once, when the `def` executes, so every call that omits the argument receives the very same object and `is _MISSING` matches. Creating the marker *inside* the function body instead would produce a fresh object per call and the identity test would never fire. **Why `object()`.** A bare `object` instance is the smallest thing Python can make that is guaranteed distinct from every other object. It inherits identity-based equality and hashing, so `==` against anything else is False; it has no state to collide on; and there is no way for a caller to construct "the same" one. Contrast the usual wrong choices. A magic string such as `'NOT_SET'` is a perfectly ordinary string a caller may legitimately pass, and equality would match a different string object with the same characters. `-1` collides with real numbers. `False` and `0` are falsy and equal to each other. `NotImplemented` and `Ellipsis` are shared built-in singletons that other libraries also use, so they are not yours. **Compare with `is`, never `==`.** The sentinel's whole value is uniqueness of identity. Writing `arg == _MISSING` invites a caller-supplied object with a permissive `__eq__` to claim to be the sentinel, and it also breaks silently for any type whose equality is element-wise rather than boolean. **Naming and visibility.** A leading underscore says "private": callers are not expected to pass it, and code that receives it from outside is misusing the API. Some libraries deliberately publish their sentinel instead, because callers genuinely need to say "restore the default" - that is a design decision, and once published the sentinel is part of your compatibility surface. **Making it readable.** `repr(object())` prints `<object object at 0x...>`, which is noise in a traceback, a log line or a rendered signature. The usual refinement is a one-instance private class: ```python class _MissingType: def __repr__(self): return "MISSING" _MISSING = _MissingType() ``` That costs four lines and makes `update(name, alias=MISSING)` legible in help output and error messages. The standard library uses the same shape for its own markers. **Standard-library markers worth naming in an interview.** `dataclasses.MISSING` is the object a field's `default` and `default_factory` hold when none was given - `None` could not be used because `None` is a perfectly good field default. `inspect.Parameter.empty` (the same object as `inspect.Signature.empty`) marks a parameter with no default and a signature with no return annotation, for exactly the same reason. `argparse.SUPPRESS` marks an argument that should be omitted from the parsed namespace rather than set to `None`. `unittest.mock.DEFAULT` tells a mock's `side_effect` to fall through to the normal return value instead of returning `None`. There is no single blessed sentinel type in the standard library; each module defines its own. **The alternative: keyword-argument presence.** You can drop the parameter from the signature and inspect `**kwargs`: ```python def update(name, **changes): if "alias" in changes: ... ``` This is honest about the three states and needs no sentinel, but it erases the signature - editors, documentation tooling and type checkers can no longer see the parameter, and typos in the keyword become silent no-ops instead of errors. Prefer the sentinel for a fixed set of optional fields, and `**kwargs` only for a genuinely open one. **One caution to carry forward.** A sentinel's guarantee is identity, and identity is local to one interpreter's object graph. The moment the value is pickled to a worker process, deep-copied, or its defining module is imported twice, the `is` check can start failing - which is a separate and very real production failure mode.
- Why not use a string such as 'NOT_SET' or the number -1 as the marker?Both are values a caller can legitimately supply, and both compare equal to any other object with the same value, so the marker is not unique. `object()` is guaranteed distinct from everything else and equal only to itself, which is the entire property you are buying. A magic number additionally shrinks the parameter's legal domain, which is a contract change nobody documented.
- What exactly does `dataclasses.MISSING` mark?That a field was declared with no default and no default factory, so the generated `__init__` must require it. `None` could not serve, because `None` is a perfectly reasonable field default and the two states must be distinguishable. It is a module-level singleton meant only for comparison with `is`; passing it as a real field value is a misuse.
- Why give the sentinel a class with a `__repr__` rather than using a bare `object()`?Readability at the moments you need it most. A bare instance prints as `<object object at 0x...>` in tracebacks, log lines and rendered signatures, which tells a reader nothing. A four-line private class whose `__repr__` returns `MISSING` costs nothing at runtime and makes help output and error messages self-explanatory. It also gives you somewhere to hang pickling behaviour later.
A sentinel is a blank ticket printed in a colour only your office has. Anyone can hand you a red or a white ticket, but nobody outside can produce that one shade.
saying these in an interview costs you the question
- Uses None as the marker in an API where None is meaningful
- Compares the sentinel with `==` instead of `is`
- Picks -1 or an empty string as the missing marker
- Creates the sentinel inside the function, so `is` never matches
- Thinks `dataclasses.MISSING` is another spelling of None
- Lets callers pass the sentinel and treats it as a real value