fix(python): stop defaulting env-var credentials to an empty string in usage snippets

LU
1d ago
+17-0

Why

When envVarPrefix is configured, Python usage snippets (README, USAGE.md, docs/sdks/*/README.md) read security values from the environment like this:

with Acme(
    api_key=os.getenv("ACME_API_KEY", ""),
) as a_client:

If the variable is unset, the snippet passes an explicit "". The SDK treats that as a provided credential, so it never takes its "no credential" path and instead sends an empty value. For a bearer scheme that is a literal Authorization: Bearer header. Passing None leaves the credential absent, which is what the snippet means.

Checked against an SDK generated from a single bearer-scheme spec (envVarPrefix: ACME, flattenGlobalSecurity: true), with ACME_API_KEY unset unless noted:

Constructor argumentAuthorization sent
api_key=os.getenv("ACME_API_KEY", "")Bearer
api_key=os.getenv("ACME_API_KEY")not sent
api_key=os.getenv("ACME_API_KEY"), variable set to secretBearer secret

What changed

  • languageSpecificEnvVarWrapping in templates/templates/pythonv2/includes/sanitization.ts now takes the fieldDef that the shared security-usage code already passes. It renders os.getenv("NAME") with no default when the x-env directive has no default and the target field is optional or nullable.
  • Typing: when the env-var security feature is enabled, global security is optional. A flattened single-scheme global security parameter is then generated as Optional[Union[Optional[str], Callable[[], Optional[str]]]] = None, so Optional[str] from os.getenv is accepted. A usage snippet extracted from the generated USAGE.md passes pyright and mypy --strict.
  • Required str fields keep the "" default. This covers fields inside security option models and required per-operation security models. Passing Optional[str] there would fail type checking and pydantic validation. In those positions an explicit security object is already being built, so the SDK's environment fallback does not apply either way.
  • Server URL and other non-security x-env usages are unchanged, because no fieldDef reaches the wrapper on those paths.
  • Generated review SDK: zSDKs/sdk-pythonv2 regenerates byte-identical. Its env-var snippets all target required str fields inside security option models or per-operation security models, so none of them change.
  • Changeset added for envVarSecurityUsage / pythonv2.

Testing

  • npm run format, make check-template-pythonv2, git diff --check: clean.
  • mise run test:generator: passed.
  • TARGET=review make build-pythonv2: no diff in zSDKs/; generation-time pyright, mypy and pylint passed.
  • TARGET=quaternary make test-pythonv2 (flattened security with envVarPrefix): 247 passed, 1 skipped. Generated tests already use a non-empty "value" default and are unaffected.
  • Generated a Python SDK from a minimal single bearer-scheme spec before and after the change. Snippets changed from os.getenv("ACME_API_KEY", "") to os.getenv("ACME_API_KEY"), SDK-level pyright/mypy/pylint passed, and the header behavior in the table above was observed with an httpx.MockTransport.
  • Not run: the full make test matrix.

Public-safety check

  • This change contains no credentials, customer documents, private repository URLs, private filesystem paths, or unredacted private logs.
  • Title, body, comments, and commit messages name no customers or customer-derived identifiers, private paths or trackers, or workflow provenance, and are understandable without private context (.claude/skills/public-repo-communication/SKILL.md).
  • Generated fixtures and review SDK changes are public-safe.
  • I reviewed git diff --check.

Summary by cubic

Fixes Python usage snippets so unset environment credentials are left absent instead of being sent as empty strings.

  • With envVarPrefix set, snippets emitted os.getenv("ACME_API_KEY", ""), which the SDK treats as a provided credential and sends as an empty value, such as an Authorization: Bearer header.
  • Optional or nullable security fields now render os.getenv("ACME_API_KEY") without a default; the generated constructor already accepts None.
  • Required str fields keep the "" default so the snippets remain type-correct under pyright and mypy --strict.
  • Non-security x-env usages such as server URLs are unchanged.