You override a chart’s default livenessProbe with an exec command, deploy, and Kubernetes rejects it: “may not specify more than one handler type.” The chart’s default httpGet probe is still there, merged underneath your override, and now the pod spec has two probe handlers. You didn’t add it. You can’t see it in your values file. And the fix is a single character: ~.
That ~ is YAML’s null, and in Helm it does something most people never learn: setting a key to null deletes it from the merged values entirely, instead of setting it to a null value. It’s the cleanest way to remove a default a chart baked in — and it’s the tip of a whole set of null-handling behaviors (absent vs null vs empty, --set foo=null, default, required, hasKey) that quietly decide whether your templates render valid YAML or foo: <no value>.
This guide covers the null-deletes-a-key trick in depth, where it works and where it bites (the --reuse-values trap, the Helm 4 regression), and the related functions you need to tell “the user didn’t set this” apart from “the user set this to empty.” Everything is verified against current Helm docs and behavior.
First: ~ is just YAML null
Before Helm, this is pure YAML. All of these mean the same thing — null:
a: ~ # canonical shorthand
b: null
c: Null
d: NULL
e: # empty value is also null~ is the canonical short form in the YAML spec; null/Null/NULL/empty are equivalent spellings. In a Helm values.yaml, writing foo: ~ is identical to foo: null. People reach for ~ because it’s terse and unmistakable — an empty value (foo:) is easy to misread as “I forgot to fill this in,” whereas foo: ~ reads as a deliberate null.
The Killer Trick: null Deletes a Default Key
Here is the behavior that makes ~ worth an article. When you override a chart’s values — with a -f values file, a parent chart overriding a subchart, or --set — Helm merges your values on top of the chart’s defaults. A normal override replaces a value. But a null override is special: Helm removes the key from the result.
Straight from the Helm docs (Chart Template Guide → Values Files):
“If you need to delete a key from the default values, you may override the value of the key to be
null, in which case Helm will remove the key from the overridden values merge.”
The canonical example is the liveness-probe foot-gun from the intro. Say the chart defaults to:
# chart's values.yaml
livenessProbe:
httpGet:
path: /healthz
port: 8080You want an exec probe instead. If you just add your exec, the merge keeps the default httpGet — and a probe with both exec and httpGet is invalid. You delete the default with null:
# your override values.yaml
livenessProbe:
httpGet: ~ # ← deletes the chart's default httpGet
exec:
command: [cat, docroot/CHANGELOG.txt]Or on the command line, exactly as the Helm docs show it:
helm install stable/drupal \
--set livenessProbe.exec.command='{cat,docroot/CHANGELOG.txt}' \
--set livenessProbe.httpGet=nullThe result has only the exec handler. Without the httpGet=null, both survive and Kubernetes rejects the manifest.
Why it works: Helm’s value coalescing walks the override tree onto the defaults. A present key with a real value overwrites; a present key with null is treated as an instruction to remove. This is the only way to subtract from a chart’s defaults — there’s no --unset flag (more on that below).
Where the null-delete works
The same mechanism applies anywhere Helm coalesces values:
- A
-foverride file on top of the chart’svalues.yaml(the docs example above). - A parent chart overriding a subchart. In the parent’s
values.yaml, nulling a key under the subchart’s name deletes that subchart default: - “`yaml
- # parent values.yaml
- mysubchart:
- someDefault: ~ # remove a default the subchart shipped
- “`
--set key=nullat install/upgrade time.
--set foo=null vs --set-string foo=null
On the CLI the distinction matters, and it’s a common source of “why didn’t it delete?”:
| Command | Result |
|---|---|
--set foo=null | foo becomes a real nil → key is deleted |
--set-string foo=null | foo becomes the literal string "null" (no deletion) |
--set a=null,name=[] | a: null, name: [] |
Helm’s --set parser does type conversion: the literal null (case-insensitive) is converted to a Go nil, which triggers the delete. --set-string forces everything to stay a string, so null is just the four-character word "null" — which is almost never what you want here. If your deletion isn’t happening, check you didn’t reach for --set-string.
Version Support (and the Helm 4 warning)
This is where “desde qué versión” gets interesting:
- Helm 2 introduced deleting a key by setting it to
null, but only reliably for top-level keys. Nested null-deletion (likeweb.livenessProbe.httpGet: null) was buggy — it could emitCannot overwrite table item ... with non table valueand override with null instead of deleting. - Helm 3 is where the documented behavior works as advertised, top-level and nested. If you’re on Helm 3, everything above is solid.
- ⚠️ Helm 4 — currently a regression. As of a still-open issue (filed March 2026), Helm 4 no longer reliably deletes keys via null: both
foo:(blank) andfoo: nullcan fail schema validation with errors likeInvalid value: "null": ... must be of type string, especially against strict Kubernetes 1.34+ schemas. If you’ve moved to Helm 4, test null-deletion before relying on it — the behavior that was rock-solid in Helm 3 is in flux. This interacts directly with values JSON schema validation: a schema that types a field asstringwill reject anull, so schema and null-deletion can fight each other.
The --reuse-values Trap
The one place null-deletion does not do what you’d hope: helm upgrade --reuse-values.
--set foo=null deletes a key relative to the chart’s defaults. It does not cleanly “un-set” a value you previously set explicitly and are now carrying forward with --reuse-values — it overrides it with null rather than falling back to the chart default. There is a long-standing feature request for an explicit --unset flag precisely because this case has no clean answer today.
Practical rule: to genuinely reset a value back to the chart default on upgrade, prefer re-specifying your full intended values (-f) over leaning on --reuse-values plus --set x=null.
Absent vs null vs empty: The Trio That Trips Everyone
Deleting keys is half the story. The other half is reading optional values in templates — and Helm/Sprig blur three states that feel different: key absent, key present but null, and key present but empty (0, "", [], {}, false).
The critical thing to internalize: default, required, empty, and coalesce all treat nil, 0, "", empty list/map, and false as the same “empty.” They cannot tell “unset” from “set to zero.”
replicas: 0 # a DELIBERATE zero...
```
```gotemplate
{{ .Values.replicas | default 3 }} # ...renders 3, not 0 — surprise!Here’s what each tool actually does:
| You want to… | Use | Behavior | |
|---|---|---|---|
| Provide a fallback for empty/unset | `{{ .Values.foo \ | default “x” }}` | Returns "x" if foo is nil, 0, "", [], {}, or false |
| Fail loudly if unset | {{ required "foo is required" .Values.foo }} | Errors on nil and on empty string (same “empty” rule as default) | |
| First non-empty of several | {{ coalesce .Values.a .Values.b "x" }} | Skips every empty/null, returns first real value | |
| Tell present-but-null from absent | {{ if hasKey .Values "foo" }} | The only reliable presence check — true even when the value is null | |
| Safely read a nested optional | {{ dig "a" "b" "fallback" .Values }} | Walks .a.b, returns "fallback" if any level is missing | |
| Branch on a condition | {{ ternary "yes" "no" .Values.enabled }} | "yes" if truthy, "no" if empty/false |
If you need to honor a deliberate 0 or false, default is wrong — use hasKey to check presence explicitly:
replicas: {{ if hasKey .Values "replicas" }}{{ .Values.replicas }}{{ else }}3{{ end }}The Rendering Foot-gun: <no value> vs null vs ""
The nastiest null bug isn’t logic — it’s a null leaking into your YAML as a broken string. The same nil value renders three different ways:
foo: {{ .Values.foo }} # → foo: <no value> ❌ invalid YAML-ish garbage
foo: {{ .Values.foo | toYaml }} # → foo: null ✅ valid YAML null
foo: {{ .Values.foo | quote }} # → foo: "" ✅ valid empty stringBare-printing an unset value gives you the literal text <no value> in the manifest — which is not null, not empty, just a string that will confuse Kubernetes or your reader. (You may also see <nil> in some contexts; the exact literal is a Go-template detail that has shifted across Helm 3 minor versions, so don’t hard-code assumptions about which one appears — the point is it’s not what you want.)
The fixes:
- Piping through
toYamlturns nil into a propernull— use it for whole objects/maps:{{ .Values.config | toYaml | nindent 2 }}. - Piping through
quoteturns nil into""— use it for optional scalars that must be strings. - Best of all, skip the key entirely when empty with
with: - “`gotemplate
- {{- with .Values.foo }}
- foo: {{ . | quote }}
- {{- end }}
- “`
- The
withblock is skipped for any empty/nil value, so an unsetfooproduces no line at all — the idiomatic Helm “omitempty.”
Switching Off a Subchart Block with null
One more practical use of ~. Because nil is “empty,” setting a value to null makes an {{ if }} guard fall through:
# subchart default turns something on
metrics:
serviceMonitor:
enabled: true
```
```yaml
# parent override switches it off cleanly
metrics:
serviceMonitor: ~ # or: enabled: falseNulling the whole block (or the flag) makes {{ if .Values.metrics.serviceMonitor }} evaluate false — a tidy way for a parent chart or an environment override to disable a section a subchart enabled by default, using the same empty-semantics as everything above.
Cheat Sheet
| Goal | Syntax | ||
|---|---|---|---|
| Write a null in values | foo: ~ (or foo: null) | ||
| Delete a chart’s default key | override it with ~ / null | ||
| Delete via CLI | --set foo=null (not --set-string) | ||
| Fallback for empty/unset | `{{ .Values.foo \ | default “x” }}` | |
| Distinguish null from absent | {{ if hasKey .Values "foo" }} | ||
| Require a value | {{ required "msg" .Values.foo }} | ||
| Nested optional read | {{ dig "a" "b" "fallback" .Values }} | ||
| Null-safe object into YAML | `{{ .Values.obj \ | toYaml \ | nindent 2 }}` |
| Omit a key when empty | {{- with .Values.foo }} … {{- end }} |
Wrapping Up
~ in Helm is a two-job character: it writes a YAML null, and — the part almost nobody documents in their own charts — it deletes a default key from the merged values, which is the only clean way to subtract a probe, an annotation, or a whole block that a chart shipped by default. Around it sits a set of null rules worth memorizing: default/required/empty can’t tell unset from zero (use hasKey when that matters), and an unset value bare-printed becomes <no value> unless you route it through toYaml, quote, or with.
Two warnings to carry: --reuse-values doesn’t cleanly un-set values, and Helm 4’s null-deletion is currently a regression — so if you’re on 4.x, verify before you rely on it.
For more Helm depth, see the companion guides on values JSON schema validation (which interacts directly with null handling), loading external files into ConfigMaps and Secrets, and what’s new in Helm 4.
Sources
- Helm docs — Values Files (“Deleting a default key”)
- Helm docs — Using Helm (
--setformat and limitations) - Helm docs — Template Function List (default, required, coalesce, ternary, dig, hasKey)
- Sprig — default / empty / coalesce
- YAML null spec
- Helm 4 null-handling regression (issue #31943)
- Feature request:
--unsetfor--reuse-values(issue #12307)
