Skip to content

feat: Add NOT_GIVEN sentinel for nullable optional params - #694

Merged
gjtorikian merged 2 commits into
mainfrom
devin/nullable-clearing-runtime
Jul 21, 2026
Merged

feat: Add NOT_GIVEN sentinel for nullable optional params#694
gjtorikian merged 2 commits into
mainfrom
devin/nullable-clearing-runtime

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Description

Hand-maintained runtime half of the cross-SDK nullable-clearing feature. Touches only the @oagen-ignore-file src/workos/_types.py (plus a test), so it won't be auto-closed by workos-sdk-automation.

Adds a NotGiven type and NOT_GIVEN sentinel:

class NotGiven:
    def __bool__(self) -> Literal[False]:
        return False
    def __repr__(self) -> str:
        return "NOT_GIVEN"

NOT_GIVEN = NotGiven()

Generated methods use NOT_GIVEN as the default for nullable optional params (e.g. external_id: Union[str, None, NotGiven] = NOT_GIVEN), so an omitted argument ("leave unchanged", filtered out) is distinguishable from an explicit None ("clear", sent as JSON null). The base client already passes the built dict straight to httpx without a second None-strip, so None reaches the wire as null.

The generated signatures/body logic come from regeneration against workos/oagen-emitters#189; this PR is the runtime prerequisite and is inert on its own.

Part of the cross-SDK effort (Ruby workos/workos-ruby#522).

Testing

Added tests/test_types.py covering the sentinel's falsiness, repr, and distinctness from None.

Documentation

Does this require changes to the WorkOS Docs? E.g. the API Reference or code snippets need updates.

[ ] Yes

If yes, link a related docs PR and add a docs maintainer as a reviewer. Their approval is required.

Link to Devin session: https://app.devin.ai/sessions/127c630f46c54bc0be571814c75047e8

Adds the NotGiven type and NOT_GIVEN sentinel to the hand-maintained
_types module. Generated methods use it as the default for nullable
optional params so an omitted argument (leave unchanged) is
distinguishable from an explicit None (clear via JSON null). The
generated signatures arrive via regeneration from oagen-emitters#189.
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author
Original prompt from heather

SYSTEM:
=== BEGIN THREAD HISTORY (in #dse-pre-triage) ===
<most_recent_message>
Heather Faerber (U08CUNLUBT9): @Devin can you update the rubygem based on this feedback?

&gt; Is there a way via the API or rubygem to clear the external_id for an organization (or user)?
&gt;
&gt; In the web UI, we can just delete the field. (It's under Settings | Organization details | Edit details | External ID.)
&gt;
&gt; But when I tried to set it with the rubygem, it doesn't work:
&gt; • nil gets stripped. The request succeeds, but the value doesn't change.
&gt; • "" returns Validation failed
&gt; • Doing the request directly also gets 200, but the value also doesn't change.
&gt;
&gt; ```&gt; org = WorkOS.client.organizations.create_organization(name: "ExtId Clear Test A", external_id: "ext-clear-test-a")
&gt; =&gt; #&lt;WorkOS::Organization object="organization" id="org_01KXT2M2RQYV33VRKJC7CKKNMY" name="ExtId Clear Test A" domains=[] metadata={} external_id="ext-clear-test-a" created_at="2026-07-18T07:39:00.241Z" updated_at="2026-07-18T07:39:00.241Z" allow_profiles_outside_organization=false&gt;
&gt;
&gt; &gt; WorkOS.client.organizations.update_organization(id: org.id, external_id: "")
&gt; (artemis):48:in '&lt;main&gt;': Validation failed (WorkOS::UnprocessableEntityError)
&gt;
&gt; &gt; WorkOS.client.organizations.update_organization(id: org.id, external_id: nil)
&gt; =&gt; `#`&lt;WorkOS::Organization object="organization" id="org_01KXT2M2RQYV33VRKJC7CKKNMY" name="ExtId Clear Test A" domains=[] metadata={} external_id="ext-clear-test-a" created_at="2026-07-18T07:39:00.241Z" updated_at="2026-07-18T07:39:18.505Z" allow_profiles_outside_organization=false&gt;
&gt;
&gt; &gt; WorkOS.client.organizations.get_organization(id: org.id).external_id.inspect
&gt; =&gt; ""ext-clear-test-a""
&gt;
&gt; &gt; WorkOS.client.request(method: :put, path: "/organizations/`#`{org.id}", body: { "external_id" =&gt; nil })
&gt; =&gt; `#`&lt;Net::HTTPOK 200 OK readbody=true&gt;
&gt;
&gt; &gt; Wo... (3272 chars truncated...)

@devin-ai-integration
devin-ai-integration Bot requested review from a team as code owners July 20, 2026 22:06
@devin-ai-integration
devin-ai-integration Bot requested a review from mattgd July 20, 2026 22:06
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot changed the title Add NOT_GIVEN sentinel for nullable optional params feat: Add NOT_GIVEN sentinel for nullable optional params Jul 20, 2026
@greptile-apps

greptile-apps Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a NotGiven sentinel class and the NOT_GIVEN module-level instance to distinguish omitted nullable optional parameters ("leave unchanged") from an explicit None ("clear the field, send JSON null"). Both are exported from the public workos namespace and covered by a dedicated test file.

  • src/workos/_types.py: Adds the NotGiven class with __bool__ → False and __repr__ → "NOT_GIVEN", plus the singleton constant NOT_GIVEN = NotGiven().
  • src/workos/__init__.py: Imports and re-exports NOT_GIVEN and NotGiven in __all__, giving callers a stable public import path.
  • tests/test_types.py: New test class covering falsiness, repr, None-distinctness, and public-namespace export.

Confidence Score: 5/5

This PR is safe to merge; it introduces an inert runtime primitive with no effect on existing call sites.

The change is purely additive — a sentinel class and constant with no callers yet, plus their public re-export. Existing behaviour is entirely unchanged. The implementation correctly mirrors the established pattern used by the OpenAI Python SDK, typing is sound (Literal[False] return on __bool__), and the new tests cover all four key contracts.

No files require special attention.

Important Files Changed

Filename Overview
src/workos/_types.py Adds NotGiven sentinel class and NOT_GIVEN constant; implementation is correct and idiomatic for the pattern.
src/workos/init.py Re-exports NOT_GIVEN and NotGiven from the public namespace and adds them to __all__; straightforward and complete.
tests/test_types.py Covers the four key sentinel contracts (falsiness, repr, None-distinctness, public export); well-structured and sufficient for the scope.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Caller invokes generated method] --> B{Was nullable param supplied?}
    B -- No, uses default --> C[param = NOT_GIVEN]
    B -- Yes, explicit None --> D[param = None]
    B -- Yes, with value --> E[param = value]
    C --> F{Filter step in generated body}
    D --> F
    E --> F
    F -- NOT_GIVEN --> G[Key omitted from request dict - leave unchanged]
    F -- None --> H[Key included as null - clear the field]
    F -- value --> I[Key included with value - set to value]
    G --> J[HTTP request sent to WorkOS API]
    H --> J
    I --> J
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Caller invokes generated method] --> B{Was nullable param supplied?}
    B -- No, uses default --> C[param = NOT_GIVEN]
    B -- Yes, explicit None --> D[param = None]
    B -- Yes, with value --> E[param = value]
    C --> F{Filter step in generated body}
    D --> F
    E --> F
    F -- NOT_GIVEN --> G[Key omitted from request dict - leave unchanged]
    F -- None --> H[Key included as null - clear the field]
    F -- value --> I[Key included with value - set to value]
    G --> J[HTTP request sent to WorkOS API]
    H --> J
    I --> J
Loading

Reviews (2): Last reviewed commit: "Export NOT_GIVEN and NotGiven from publi..." | Re-trigger Greptile

Comment thread src/workos/_types.py
@gjtorikian
gjtorikian merged commit 3b9a2cb into main Jul 21, 2026
11 checks passed
@gjtorikian
gjtorikian deleted the devin/nullable-clearing-runtime branch July 21, 2026 03:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants