click-clirunner-env-none-deletes
Click CliRunner: env=None deletes; absence does NOT
Problem
You wrote a CLI test where the SUT (system under test) branches on whether an env var is set. The test invokes the CLI via CliRunner.invoke(cli, args, env=clean_env_dict), where clean_env_dict omits the env var you want absent. The test passes locally. You ship it. Later it turns out the test was never exercising the "var absent" branch — it was running with the var present (inherited from your shell), and your assertion happened to also pass for the "var present" code path. In a live-API-call test, this can mean the test SILENTLY makes the network call you thought you were preventing.
Root cause
Click's CliRunner.invoke(env=...) is an OVERRIDE dict, not a REPLACEMENT environment. It iterates the dict and applies each key:
env[key] = "value"→ setsos.environ[key] = "value"for the duration of the invocation, restoring afterwardenv[key] = None→ callsdel os.environ[key]for the duration of the invocation, restoring afterward- key absent from
env→os.environ[key]is left UNTOUCHED
Originally verified against click/testing.py:534 on Click 8.1.x; re-checked 2026-08-23 and 2026-08-24 against the current published testing module source. The durable evidence is the signature: env: Mapping[str, str | None] | None on CliRunner, CliRunner.invoke and CliRunner.isolation. A value type of str | None is the API stating that None is a meaningful value rather than an omission — that is the delete. The docs describe env as "overrides", which is the absent-keys-untouched half. Cite the signature, not a file offset or a version pin — the 8.1.x branch no longer exists, and the signature has survived every check so far.
Trigger conditions
You wrote one of these patterns and the test silently passed: