You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Python: Add string alternatives at stable enum input boundaries #9635
Maintainer-owned issue. Community contributors: please do not pick up or start implementing this issue.
Problem
Some public APIs require callers to import enums for stable input vocabularies where a Literal[...] | EnumType input could keep the common path simpler. This should be applied selectively and must not change enum-valued outputs or internal state-machine semantics.
Use Case and Evidence
Core already provides a positive normalization pattern in feature-stage handling. Candidate input boundaries include evaluation and workflow configuration enums, but each requires verification that the vocabulary is stable and naturally represented as strings.
Desired Outcome
Audit exported enum-only input parameters, add typed string alternatives where practical, normalize at the boundary, and preserve existing enum acceptance, return types, serialization, and exhaustive internal dispatch.
Alternatives Considered
Converting enums to free-form strings would weaken typing and validation. Applying the pattern to internal state enums would add no user-facing benefit.
Proposed Direction
Add only closed Literal vocabularies, test equivalent enum/string behavior, and document the import-light path. Treat stricter validation or output changes as separate compatibility work.
Scope and Non-Goals
This issue is additive. It does not remove enums or change serialized/returned values.
Language
Python
Contribution Intent
Maintainer-owned; implementation must wait for maintainer design agreement.
Acknowledgements
I searched existing issues and did not find a duplicate.
I will wait for explicit maintainer agreement before starting implementation of a non-trivial change.
Ownership: Core maintainers only; not available for community pickup.
We will consider this in core MAF, limited to typed string alternatives at selected stable public enum input boundaries. This does not commit to implementation or delivery timing.
Keep existing enum inputs, enum-valued outputs, serialization, and internal state-machine behavior unchanged. Planning should identify suitable closed vocabularies and their normalization and validation requirements. This remains maintainer-owned; it is not available for community implementation.
/plan Audit exported Python enum-only input parameters and identify stable closed vocabularies suitable for typed Literal alternatives. Use existing boundary normalization as prior art, not permission to accept strings for every enum. Preserve enum inputs, enum-valued outputs, serialization, validation behavior and exhaustive internal dispatch; treat stricter validation or output changes separately. Propose a bounded scope and enum/string parity tests. This remains maintainer-owned; provide design only, not implementation.
You want an audit of Python public inputs that currently require an enum, to find stable, closed vocabularies that could also accept a typed Literal[...] string. Enum inputs, enum outputs, serialization, validation and internal dispatch must stay as they are. This is design only. I read code and did not run anything.
What the audit found
Enum classes in python/packages/ fall into three groups.
EvalItem(split_strategy=...), EvalItem.split_messages(split=...), and the conversation_split= parameter of evaluate_agent / evaluate_workflow. All are typed ConversationSplitter, which is a callable protocol.
ContentLabel.__init__ already does IntegrityLabel(integrity) when it is given a non-enum value. It is therefore a working example of boundary normalization, but its annotation does not say strings are accepted.
ConversationSplit is the clearest gap. Today a string is not callable, so passing "full" would fail only later, at the call. Its str base means ConversationSplit.FULL == "full", but that is not accepted by the type.
Needs a case-by-case check before deciding (outside core, vocabulary may follow a service):
Purview models and settings: Activity, ExecutionMode, PurviewLocationType (purview/_models.py, _settings.py). These look like closed vocabularies tied to a Microsoft service. Confirm each is stable and that the string values are the service's own wire values, not just local names.
ExperimentalFeature and ReleaseCandidateFeature. Their own docstring says they are not a stable inventory, so a Literal of them would go stale.
Provider content-filter enums in openai/_exceptions.py.
I found no workflow-configuration enum that is a clear input, so the issue's "workflow configuration enums" candidate is not confirmed. A maintainer who knows a specific one should name it.
Prior art, and why it is not a blanket rule:_feature_stage.py has _normalize_feature_id(feature_id: str | Enum). It works because feature IDs are labels only. Many enum inputs carry behaviour or are experimental, so each one needs its own justification.
Recommendation
Code changes are warranted, but only as a small, additive pass. This is a nice-to-have convenience, not a defect, and existing code is unaffected. I would leave everything outside the strong-candidate table alone unless a maintainer confirms it case by case.
Proposed plan (proposal only; nothing has been changed)
Agree the inclusion rule. An enum qualifies only if all of these hold: it is public and used as an input; its set of values is closed and unlikely to grow; its string values are documented and intended; and enum and string are exactly equivalent. Experimental status is not an exclusion, but the new accepted type changes the experimental surface, so mention it in the PR.
Evaluation. Add ConversationSplitLiteral = Literal["last_turn", "full"], or a similar private alias. Widen the annotations to ConversationSplitter | ConversationSplitLiteral on EvalItem.__init__, split_messages and the two conversation_split parameters. Normalize once with a small helper: if the value is a str (including the enum, which is already a str), convert it with ConversationSplit(value). Callables pass through untouched. _BUILT_IN_SPLITTERS and the stored/returned type stay the same. Decide whether EvalItem.split_strategy stores the normalized enum. I suggest yes, so reads still return the enum.
Security labels. Widen the annotations to IntegrityLabel | Literal["trusted", "untrusted"] and the confidentiality equivalent. Normalize at each public entry point. Keep all stored values and comparisons as enums, so ordering and threshold logic is unchanged. Reuse the existing ContentLabel conversion instead of adding a new path.
Validation. Keep today's behaviour: an unknown string raises the same ValueError the enum constructor gives. Any stricter or friendlier validation (case folding, whitespace trimming, custom messages) is separate compatibility work. Do not add it here.
Output and serialization. No changes. to_dict and from_dict output, return values and attributes stay enums. Add tests that assert this.
Parity tests (core tests for evaluation and security), parameterized over the vocabulary:
For each member, the enum and its string give equal results: same split output, same label attributes, same to_dict().
The stored attribute is the enum in both cases.
An invalid string raises ValueError, the same as before.
A custom callable still works for ConversationSplit.
A static typing check that Literal values are accepted and typos are rejected (via the existing Pyright run).
A drift guard that fails if the enum gains a member that the Literal lacks, using get_args(...) against the enum values.
Docs. Update docstrings and the core AGENTS.md note on these APIs to show the import-light form. Do not update the consumer skills under skills/ unless an existing recommendation becomes wrong.
Out of scope until separately approved: Purview, shell and ag-ui enums (decide after a short per-enum check); all output and state enums; any change to validation strictness.
Done when the two groups above accept both forms with identical behaviour, the parity and drift tests pass, and pyright/mypy show no new errors.
Trade-offs to be aware of: every Literal duplicates the enum's values, so the drift guard matters. For experimental APIs the extra accepted type is an additional surface to maintain. Each accepted string also becomes a public contract.
Generated by Issue Planning for #9635 · copilot · auto · ~41 AIC · ⌖ ~1 AIC · ⊞ 4.8K · ◷
Activity
eavanvalkenburg commentedon Nov 9, 2026
We will consider this in core MAF, limited to typed string alternatives at selected stable public enum input boundaries. This does not commit to implementation or delivery timing.
Keep existing enum inputs, enum-valued outputs, serialization, and internal state-machine behavior unchanged. Planning should identify suitable closed vocabularies and their normalization and validation requirements. This remains maintainer-owned; it is not available for community implementation.
eavanvalkenburg commentedon Nov 9, 2026
/plan Audit exported Python enum-only input parameters and identify stable closed vocabularies suitable for typed Literal alternatives. Use existing boundary normalization as prior art, not permission to accept strings for every enum. Preserve enum inputs, enum-valued outputs, serialization, validation behavior and exhaustive internal dispatch; treat stricter validation or output changes separately. Propose a bounded scope and enum/string parity tests. This remains maintainer-owned; provide design only, not implementation.
github-actions commentedon Nov 9, 2026
Summary
You want an audit of Python public inputs that currently require an enum, to find stable, closed vocabularies that could also accept a typed
Literal[...]string. Enum inputs, enum outputs, serialization, validation and internal dispatch must stay as they are. This is design only. I read code and did not run anything.What the audit found
Enum classes in
python/packages/fall into three groups.Strong candidates (public input, small closed vocabulary, string values already exist):
ConversationSplit(core/_evaluation.py, experimentalEVALS)EvalItem(split_strategy=...),EvalItem.split_messages(split=...), and theconversation_split=parameter ofevaluate_agent/evaluate_workflow. All are typedConversationSplitter, which is a callable protocol."last_turn","full"IntegrityLabel,ConfidentialityLabel(core/security.py, experimentalFIDES)ContentLabel(...), plusdefault_integrity,default_confidentiality,hide_thresholdandmax_allowedon several security middleware and helpers"trusted"/"untrusted";"public"/"private"/"user_identity"ContentLabel.__init__already doesIntegrityLabel(integrity)when it is given a non-enum value. It is therefore a working example of boundary normalization, but its annotation does not say strings are accepted.ConversationSplitis the clearest gap. Today a string is not callable, so passing"full"would fail only later, at the call. Itsstrbase meansConversationSplit.FULL == "full", but that is not accepted by the type.Needs a case-by-case check before deciding (outside core, vocabulary may follow a service):
Activity,ExecutionMode,PurviewLocationType(purview/_models.py,_settings.py). These look like closed vocabularies tied to a Microsoft service. Confirm each is stable and that the string values are the service's own wire values, not just local names.ShellFamily(tools/shell/_environment.py,override_family).ClaimRecoveryPolicy(ag-ui/_approval_lifecycle.py).Not suitable, or no input benefit:
WorkflowRunState,WorkflowEventSource,BackgroundTaskStatus,EdgeGroupDeliveryStatus,MessageType,ApprovalStatus.MiddlewareType,_ArchiveFormat,FeatureIndex,OtelAttr,ValidationTypeEnum.ExperimentalFeatureandReleaseCandidateFeature. Their own docstring says they are not a stable inventory, so aLiteralof them would go stale.openai/_exceptions.py.I found no workflow-configuration enum that is a clear input, so the issue's "workflow configuration enums" candidate is not confirmed. A maintainer who knows a specific one should name it.
Prior art, and why it is not a blanket rule:
_feature_stage.pyhas_normalize_feature_id(feature_id: str | Enum). It works because feature IDs are labels only. Many enum inputs carry behaviour or are experimental, so each one needs its own justification.Recommendation
Code changes are warranted, but only as a small, additive pass. This is a nice-to-have convenience, not a defect, and existing code is unaffected. I would leave everything outside the strong-candidate table alone unless a maintainer confirms it case by case.
Proposed plan (proposal only; nothing has been changed)
Agree the inclusion rule. An enum qualifies only if all of these hold: it is public and used as an input; its set of values is closed and unlikely to grow; its string values are documented and intended; and enum and string are exactly equivalent. Experimental status is not an exclusion, but the new accepted type changes the experimental surface, so mention it in the PR.
Evaluation. Add
ConversationSplitLiteral = Literal["last_turn", "full"], or a similar private alias. Widen the annotations toConversationSplitter | ConversationSplitLiteralonEvalItem.__init__,split_messagesand the twoconversation_splitparameters. Normalize once with a small helper: if the value is astr(including the enum, which is already astr), convert it withConversationSplit(value). Callables pass through untouched._BUILT_IN_SPLITTERSand the stored/returned type stay the same. Decide whetherEvalItem.split_strategystores the normalized enum. I suggest yes, so reads still return the enum.Security labels. Widen the annotations to
IntegrityLabel | Literal["trusted", "untrusted"]and the confidentiality equivalent. Normalize at each public entry point. Keep all stored values and comparisons as enums, so ordering and threshold logic is unchanged. Reuse the existingContentLabelconversion instead of adding a new path.Validation. Keep today's behaviour: an unknown string raises the same
ValueErrorthe enum constructor gives. Any stricter or friendlier validation (case folding, whitespace trimming, custom messages) is separate compatibility work. Do not add it here.Output and serialization. No changes.
to_dictandfrom_dictoutput, return values and attributes stay enums. Add tests that assert this.Parity tests (core tests for evaluation and security), parameterized over the vocabulary:
to_dict().ValueError, the same as before.ConversationSplit.Literalvalues are accepted and typos are rejected (via the existing Pyright run).Literallacks, usingget_args(...)against the enum values.Docs. Update docstrings and the core
AGENTS.mdnote on these APIs to show the import-light form. Do not update the consumer skills underskills/unless an existing recommendation becomes wrong.Out of scope until separately approved: Purview, shell and ag-ui enums (decide after a short per-enum check); all output and state enums; any change to validation strictness.
Done when the two groups above accept both forms with identical behaviour, the parity and drift tests pass, and pyright/mypy show no new errors.
Trade-offs to be aware of: every
Literalduplicates the enum's values, so the drift guard matters. For experimental APIs the extra accepted type is an additional surface to maintain. Each accepted string also becomes a public contract.