Skip to content

Python: Add string alternatives at stable enum input boundaries #9635

Open

Description

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.

Activity

  1. eavanvalkenburg commented on Nov 9, 2026

    @eavanvalkenburg
    MemberAuthor

    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.

  2. eavanvalkenburg commented on Nov 9, 2026

    @eavanvalkenburg
    MemberAuthor

    /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.

  3. github-actions commented on Nov 9, 2026

    @github-actions
    Contributor

    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):

    Enum Where it is an input Values
    ConversationSplit (core/_evaluation.py, experimental EVALS) 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. "last_turn", "full"
    IntegrityLabel, ConfidentialityLabel (core/security.py, experimental FIDES) ContentLabel(...), plus default_integrity, default_confidentiality, hide_threshold and max_allowed on several security middleware and helpers "trusted"/"untrusted"; "public"/"private"/"user_identity"
    • 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.
    • ShellFamily (tools/shell/_environment.py, override_family).
    • ClaimRecoveryPolicy (ag-ui/_approval_lifecycle.py).

    Not suitable, or no input benefit:

    • Output or state enums: WorkflowRunState, WorkflowEventSource, BackgroundTaskStatus, EdgeGroupDeliveryStatus, MessageType, ApprovalStatus.
    • Internal enums: MiddlewareType, _ArchiveFormat, FeatureIndex, OtelAttr, ValidationTypeEnum.
    • 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)

    1. 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.

    2. 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.

    3. 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.

    4. 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.

    5. Output and serialization. No changes. to_dict and from_dict output, return values and attributes stay enums. Add tests that assert this.

    6. 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.
    7. 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.

    8. 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 · ◷

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    pythonUsage: [Issues, PRs], Target: Python

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions