Skip to content

Fix negative type narrowing for mapping patterns with multiple key entries - #11612

Open
Henry Su (hsusul) wants to merge 2 commits into
microsoft:mainfrom
hsusul:fix/mapping-pattern-multi-key-narrowing
Open

Fix negative type narrowing for mapping patterns with multiple key entries#11612
Henry Su (hsusul) wants to merge 2 commits into
microsoft:mainfrom
hsusul:fix/mapping-pattern-multi-key-narrowing

Conversation

@hsusul

Copy link
Copy Markdown
Contributor

Minimal Reproduction Case

from typing import Literal, TypedDict

class MsgA(TypedDict):
    v: Literal[1]
    kind: Literal["a"]
    data_a: int

class MsgB(TypedDict):
    v: Literal[1]
    kind: Literal["b"]
    data_b: str

def test_negative_narrowing(msg: MsgA | MsgB) -> None:
    match msg:
        case {"v": 1, "kind": "a"}:
            reveal_type(msg)  # MsgA
        case _:
            reveal_type(msg)  # Expected: MsgB, Actual pre-fix: MsgA | MsgB

Current vs Expected Behavior

  • Current Behavior: When a mapping pattern contains two or more key entries (e.g. case {"v": 1, "kind": "a"}:), Pyright does not eliminate matching TypedDict subtypes from the subject in the fallthrough (case _:) branch. Consequently, reveal_type(msg) in case _: reports MsgA | MsgB.
  • Expected Behavior: Any subject instance matching case {"v": 1, "kind": "a"}: is guaranteed to be MsgA. The fallthrough case _: branch cannot receive MsgA, so msg must be narrowed to MsgB.

Rationale

In Python 3.10+ pattern matching (PEP 634), matching a subject against a mapping pattern checks whether the subject is a mapping and contains matching values for all specified key entries. When a subject is a union of TypedDict types, a TypedDict subtype whose literal field values match all key entries of a mapping pattern will always match that case clause. In negative type narrowing (!isPositiveTest), such matching TypedDict subtypes must be eliminated from the remaining subject type in fallthrough branches.

Root Cause

In packages/pyright-internal/src/analyzer/patternMatching.ts, narrowTypeBasedOnMappingPattern previously hardcoded a single-key check (pattern.d.entries.length === 1) when !isPositiveTest. Mapping patterns with two or more key entries were skipped, preventing negative type narrowing for multi-key patterns.

Proposed Fix

Generalize narrowTypeBasedOnMappingPattern under !isPositiveTest to parse all key entries in pattern.d.entries. If a TypedDict subtype matches all key entries in the mapping pattern (matching string literal key names, literal value types, or wildcard/capture variables), eliminate that TypedDict subtype from the remaining type.

Test Coverage

Added regression test test_negative_narrowing3 to packages/pyright-internal/src/tests/samples/matchMapping1.py.

Validation Results

  • npx jest src/tests/typeEvaluator6.test.ts passed (148/148 tests passed).
  • npx tsc --noEmit passed with 0 errors.
  • git diff --check passed with 0 formatting or whitespace errors.

@StellaHuang95

Stella Huang (StellaHuang95) commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

🔒 Automated review in progress — Stella Huang (@StellaHuang95) is auto-reviewing this PR.

valuePattern.d.orPatterns.length === 1 &&
(valuePattern.d.orPatterns[0].nodeType === ParseNodeType.PatternCapture ||
valuePattern.d.orPatterns[0].nodeType === ParseNodeType.PatternValue)
) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Issue · Please address or respond

PatternValue is grouped with PatternCapture and therefore treated as an unconditional match (valueTypes = undefined). A value pattern such as Color.RED is an equality check, so a TypedDict whose required field contains Color.BLUE can fall through but is incorrectly eliminated here. Only treat captures as presence-only, or conservatively leave value patterns unsupported until their value can be compared soundly.

case {"v": 1, "kind": "a"}:
reveal_type(msg, expected_text="MsgA")
case _:
reveal_type(msg, expected_text="MsgB")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Warning · Non-blocking recommendation

This test covers only multi-key literal matching. Add an enum/value-pattern regression that asserts the TypedDict is retained in the fallthrough branch; it would catch the unsound PatternValue narrowing above. Coverage for the new capture-value path would also lock in its intended behavior.

@StellaHuang95 Stella Huang (StellaHuang95) added the review-auto:changes-requested Automated review: posted blocking findings to address. label Aug 10, 2026
@hsusul

Copy link
Copy Markdown
Contributor Author

Thank you Stella Huang (@StellaHuang95) for the review!

I have updated the PR (commit 08e9923d2):

  1. Excluded PatternValue from presence-only matching: Removed ParseNodeType.PatternValue from the wildcard/capture check in patternMatching.ts. Only ParseNodeType.PatternCapture is treated as a presence-only match. Value patterns (such as Color.RED) fall through to hasUnsupportedKeyPattern = true, preventing unsound elimination.
  2. Added value & capture pattern regression tests: Added tests in matchMapping1.py covering:
    • Enum/value patterns (case {"color": Color.RED}:), verifying that BlueMsg is retained in the fallthrough branch.
    • Capture variable patterns (case {"v": 1, "kind": x}:), verifying capture narrowing and fallthrough exhaustiveness.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved via Review Center.

@StellaHuang95 Stella Huang (StellaHuang95) added review-auto:approved Automated review: no blocking findings (approval posted). and removed review-auto:changes-requested Automated review: posted blocking findings to address. labels Aug 10, 2026

@rchiodo Rich Chiodo (rchiodo) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved via Review Center.

@rchiodo Rich Chiodo (rchiodo) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved via Review Center.

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

Labels

review-auto:approved Automated review: no blocking findings (approval posted).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants