Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Guidance needed: how to narrow unions of TypedDicts? #18543

Closed
magicmark opened this issue Jan 27, 2025 · 3 comments · May be fixed by #18547
Closed

Guidance needed: how to narrow unions of TypedDicts? #18543

magicmark opened this issue Jan 27, 2025 · 3 comments · May be fixed by #18547

Comments

@magicmark
Copy link

magicmark commented Jan 27, 2025

The docs give very clear guidance on how to write code using unions of TypedDicts:

https://mypy.readthedocs.io/en/stable/typed_dict.html#unions-of-typeddicts

This makes sense!

However, a common use case (for me) of a TypedDict is to allow external code (e.g. REST API input) to be deserialized into a dict and refined at runtime into a TypedDict -- which implies the the design requirement to have a branded field (e.g. tag) must now exist in our public facing API.

This could work in some cases, but consider something like this:

from __future__ import annotations
from typing import List, TypedDict

class Policy(TypedDict):
    name: str

class CombinedORPolicy(TypedDict):
    OR: List[Policy]

class CombinedANDPolicy(TypedDict):
    AND: List[Policy]


def print_policy(policy: CombinedANDPolicy | CombinedORPolicy | Policy) -> None:
    if 'OR' in policy:
        allowed_policy_names = ' '.join([p['name'] for p in policy['OR']])
        print(f"Can match _any_ of the following: {allowed_policy_names}")
    elif 'AND' in policy:
        allowed_policy_names = ' '.join([p['name'] for p in policy['AND']])
        print(f"Must match _all_ the following: {allowed_policy_names}")
    else:
        print(f"Must be exactly {policy['name']}")


# pretend this is an API endpoint or something
print_policy({'name': 'role:is_admin'})

#13838 being closed seems to imply this is possible but i'm still having trouble.

Here's the current mypy (1.14.1) output:

test.py: note: In function "print_policy":
test.py:16:68: error: TypedDict "CombinedANDPolicy" has no key "OR"  [typeddict-item]
test.py:16:68: error: TypedDict "Policy" has no key "OR"  [typeddict-item]
test.py:19:68: error: TypedDict "Policy" has no key "AND"  [typeddict-item]
Found 3 errors in 1 file (checked 1 source file)

The solution as I understand is either:

  • force an API change (e.g. print_policy({'name': 'role:is_admin', 'tag': 'root-policy'}))
  • use cast (e.g. policy = cast(CombinedANDPolicy, policy)

Is there anything more idiomatic i'm missing?

(I searched the tracker but wasn't able to find discission of this specifically, but I think this is related: #7981 #12098 #11080)

Is there a stance on this yet? Is this is a goal or non-goal of mypy? Thanks!

FWIW -- here's a version of this working in

thanks!

@A5rocks
Copy link
Collaborator

A5rocks commented Jan 27, 2025

Adding @final to all TypedDicts makes this work. See also #15697.

@A5rocks A5rocks closed this as completed Jan 27, 2025
@A5rocks
Copy link
Collaborator

A5rocks commented Jan 27, 2025

For reference, I checked:

from __future__ import annotations
from typing import List, TypedDict, final

@final
class Policy(TypedDict):
    name: str

@final
class CombinedORPolicy(TypedDict):
    OR: List[Policy]

@final
class CombinedANDPolicy(TypedDict):
    AND: List[Policy]


def print_policy(policy: CombinedANDPolicy | CombinedORPolicy | Policy) -> None:
    if 'OR' in policy:
        allowed_policy_names = ' '.join([p['name'] for p in policy['OR']])
        print(f"Can match _any_ of the following: {allowed_policy_names}")
    elif 'AND' in policy:
        allowed_policy_names = ' '.join([p['name'] for p in policy['AND']])
        print(f"Must match _all_ the following: {allowed_policy_names}")
    else:
        print(f"Must be exactly {policy['name']}")


# pretend this is an API endpoint or something
print_policy({'name': 'role:is_admin'})

and it passes without mypy complaining.

@A5rocks A5rocks closed this as not planned Won't fix, can't repro, duplicate, stale Jan 27, 2025
@magicmark
Copy link
Author

Brilliant, thanks! I might send a PR to add this to the docs here https://mypy.readthedocs.io/en/stable/typed_dict.html#unions-of-typeddicts

magicmark added a commit to magicmark/mypy that referenced this issue Jan 27, 2025
Fixes python#18543

This is a useful but also non-obvious pattern that seems worthy of a docs example :)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants