Skip to content

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

Open
LukasParke wants to merge 1 commit into
speakeasy-api:mainfrom
LukasParke:fix/python-getenv-usage-snippet
Open

LukasParke wants to merge 1 commit into
speakeasy-api:mainfrom
LukasParke:fix/python-getenv-usage-snippet

Conversation

@LukasParke

@LukasParke LukasParke commented Oct 21, 2026 •

Copy link
Copy Markdown

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 argument Authorization 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 secret Bearer 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.

Written for commit 28628f7. Summary will update on new commits.

View guided diff

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@github-actions github-actions Bot added the python Trigger (106) snapshot tests for python label Oct 21, 2026
…n usage snippets

With envVarPrefix configured, Python usage snippets read security values
from the environment as os.getenv("PREFIX_API_KEY", ""). When the
variable is unset, the snippet passes an explicit empty string, which the
SDK treats as a provided credential and sends as an empty value (for
example "Authorization: Bearer ").

Render os.getenv("PREFIX_API_KEY") without a default when the target
security field is optional or nullable. The generated constructor and
model already accept None there, so the snippet still type-checks under
pyright and mypy, and an unset variable now leaves the credential absent.
Required str fields keep the "" default so the snippet remains
type-correct.
@LukasParke
LukasParke force-pushed the fix/python-getenv-usage-snippet branch from f36edc1 to 28628f7 Compare October 21, 2026 19:13
@LukasParke
LukasParke marked this pull request as ready for review October 21, 2026 19:19
@LukasParke
LukasParke requested a review from a team as a code owner October 21, 2026 19:19
@LukasParke

Copy link
Copy Markdown
Author

recheck

@2ynn 2ynn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @LukasParke! As stated in PR#130 we want to preserve the existing distinction between explicit "" and None (i.e. an empty credential should not silently trigger environment fallback). Accordingly this PR makes sense:
an unset environment variable should remain absent rather than become an explicit empty credential.

I will approve it now, although I think we should apply that principle across other languages so they remain consistent, at least Go and Typescript that also support the automatic envVarPrefix security feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Trigger (106) snapshot tests for python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants