feat(lint): exempt TypedDict-annotated dict literals from LIT002 - #36869
Conversation
Greptile SummaryThis PR exempts dict literals assigned to TypedDict-annotated values from LIT002 while retaining checks for untyped and explicitly mutable annotations.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| scripts/check_type_discipline.py | Adds the TypedDict annotation classifier and dict-literal exemption, including the correction for PEP 604 optional unions. |
| tests/test_litellm/test_check_type_discipline.py | Covers qualifying TypedDict annotations, wrappers, negative annotation cases, nested values, and the prior PEP 604 regression. |
| type-discipline-budget.json | Ratchets the LIT002 ceiling from 27139 to 26901 to match the intended reduction. |
Reviews (4): Last reviewed commit: "Merge remote-tracking branch 'origin/lit..." | Re-trigger Greptile
c943949 to
316732b
Compare
…itellm_lit002_typeddict_dict_literals # Conflicts: # type-discipline-budget.json
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue. You can view the agent here.
Reviewed by Cursor Bugbot for commit 3120f26. Configure here.
| if head == "Annotated": | ||
| first = annotation.slice.elts[0] if isinstance(annotation.slice, ast.Tuple) and annotation.slice.elts else None | ||
| return first is not None and _is_typeddict_annotation(first) | ||
| return head is not None and head not in NON_TYPEDDICT_HEADS |
There was a problem hiding this comment.
Union annotations falsely exempt Mapping
Medium Severity
_is_typeddict_annotation treats a Union[...] subscript as a TypedDict head and never inspects its arms. Optional and PEP 604 | unwrap correctly, so Mapping/Any/object stay flagged there, but the same annotations under typing.Union wrongly earn the LIT002 exemption even though basedpyright accepts those assignments.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 3120f26. Configure here.
There was a problem hiding this comment.
ruff hard-enforces UP007/UP045 repo-wide at zero violations, so a typing.Union annotation can't land here. The PEP 604 and Optional spellings are already checked


TLDR
Problem this solves:
MyTD(a=1)calls or# mutable-oksuppressionsHow it solves it:
x: Final[MyTD] = {...}x: Final = {...}names no type and stays flaggedUser Flow
Before: a contributor who builds a fixed-shape TypedDict payload with a dict literal gets an LIT002 lint failure and must suppress or rewrite
x: Final[MyTD] = {"a": 1}to a file underlitellm/, whereMyTDis a TypedDictmake checkLIT002 mutable dict literal: this builds a collection that can be grown or rewritten, pointing at the annotated line; pushing anyway turns the CI lint job red with the same message# mutable-ok: <reason>to the line or rewrite the payload asMyTD(a=1), which cannot express keys that are not valid identifiersAfter: the same annotated dict literal passes lint, while an unannotated
Finaldict literal still failsx: Final[MyTD] = {"a": 1}to a file underlitellm/, whereMyTDis a TypedDictmake checkOK: every LIT rule is within its codebase ceiling, and the CI lint job stays greeny: Final = {"a": 1}still fails with LIT002, whose message now also names the fix:a TypedDict-annotated dict literal (x: Final[MyTD] = {...})Relevant issues
Linear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Demo file exercising the exemption and its negative case:
Before, checker at base 9d069f2 flags all three assignments:
After, checker at 316732b passes both annotated builds and still flags the bare
Final, with the fix message now naming the TypedDict escape:Gate over the whole tree at 316732b, with the ratcheted budget:
Type
🆕 New Feature
Caveats (if any)
return {...}into a TypedDict return type still tripsMapping/Any/objectannotations stay flagged toward MappingProxyTypeFinal Attestation
Note
Low Risk
Scope is limited to the type-discipline AST linter and its violation budget; runtime behavior is unchanged and false positives are bounded by existing basedpyright checks.
Overview
LIT002 no longer flags dict literals on assignments whose annotation is treated as a TypedDict (e.g.
x: Final[MyTD] = {...}), so contributors can use literal syntax—including non-identifier keys—without# mutable-okorMyTD(...)constructor calls.The checker adds a name-based annotation walk (
Final/ClassVar/Optional/Annotatedunwrap, PEP 604 unions, string forward refs) and skips those dict literals plus nested dict literals inside the value; bareFinal/ClassVar,dict/Mapping/Any/object, and non-literal builds (dict(), comprehensions, mutable values inside the tree) still trip LIT002. Rule docs and the violation message now describe the TypedDict escape hatch instead of only “ReadOnly TypedDict.”type-discipline-budget.jsonlowers the LIT002 ceiling by 238 (27139 → 26901) to match cleared violations; new unit tests cover positive and negative cases.Reviewed by Cursor Bugbot for commit 3120f26. Bugbot is set up for automated code reviews on this repo. Configure here.