Skip to content

feat(lint): exempt TypedDict-annotated dict literals from LIT002 - #36813

Closed
mateo-berri wants to merge 6 commits into
litellm_internal_stagingfrom
litellm_lit002_typeddict_literal_exemption
Closed

feat(lint): exempt TypedDict-annotated dict literals from LIT002#36813
mateo-berri wants to merge 6 commits into
litellm_internal_stagingfrom
litellm_lit002_typeddict_literal_exemption

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • LIT002 flags x: MyTd = {...} but not the MyTd(...) spelling
  • Same value and type either way, so the count is a spelling tax
  • TypedDicts with non-identifier keys can't use the call spelling at all

How it solves it:

  • Exempts dict literals assigned to names annotated with a TypedDict defined at the file's top level
  • Only if the name is bound exactly once in the file
  • Only if every declared or inherited field is ReadOnly
  • Only the outer display: nested mutable literals still count

User Flow

Before: a contributor who builds a TypedDict payload as an annotated dict literal fails the lint gate and must respell or suppress

  1. They write payload: Final[RequestKwargs] = {"model": m, "max_tokens": 128} with RequestKwargs a TypedDict defined in the same module
  2. They stage the change and run make check
  3. The type-discipline gate fails: the line is reported as LIT002 mutable dict literal and the codebase count lands one over its ratcheted ceiling
  4. To land the PR they respell the value as RequestKwargs(model=m, max_tokens=128) or add # mutable-ok: <reason>, though neither changes what the value is at runtime

After: the same literal passes the gate untouched

  1. They write the same payload: Final[RequestKwargs] = {"model": m, "max_tokens": 128} line
  2. They stage the change and run make check
  3. The type-discipline gate passes: the literal now has the same standing as the RequestKwargs(...) call, and the LIT002 count is unchanged
  4. When some other line does trip LIT002, its message now also names the annotated-literal spelling as a blessed fix

Relevant issues

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to 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 snippet td_snip.py (a TypedDict-annotated dict literal, the pattern this PR legalizes):

from typing import Final, ReadOnly, TypedDict


class RequestKwargs(TypedDict):
    model: ReadOnly[str]
    max_tokens: ReadOnly[int]


payload: Final[RequestKwargs] = {"model": "gpt-5.5", "max_tokens": 128}

Before, at base commit 7fcca52:

$ python scripts/check_type_discipline.py td_snip.py
td_snip.py:9: LIT002 mutable dict literal: this builds a collection that can be grown or rewritten. ...

1 violation(s).
$ echo $?
1

After, at bbb5c94 the same command prints nothing and exits 0

Gates and tests at bbb5c94, on top of litellm_internal_staging:

$ python scripts/type_discipline_gate.py
OK: every LIT rule is within its codebase ceiling (base origin/litellm_internal_staging)
$ python scripts/ruff_strict_gate.py
OK: every strict rule is within its codebase ceiling (base origin/litellm_internal_staging)
$ python scripts/type_check_gate.py
OK: every rule is within its basedpyright limit or no higher than base (145568 errors total)
$ pytest tests/test_litellm/test_check_type_discipline.py -q
97 passed in 0.29s

False-negative audit at bbb5c94: running the base checker and this branch's checker over litellm/ tests/ enterprise/ scripts/ produces the identical set of 162,952 LIT002 lines, so no existing site gains the exemption today and make lint-budget-update moves no ceilings ("Ratcheted LIT-rule limits down by 0"). Exempted sites are also frozen by construction: the annotation must name a module-top-level TypedDict bound exactly once whose every declared and inherited field is ReadOnly, so a writable or # writable-ok'd TypedDict still counts

Type

🚄 Infrastructure

Caveats (if any)

  • Top-level same-module TypedDicts only; imported ones still count, matching LIT012's reach, and one nested inside a def or class never qualifies since its name does not resolve outside its scope
  • TypedDicts with any writable field still count, even where LIT012 was suppressed
  • Names the file also binds in any other form (def, assignment, import, loop or walrus or unpacking target, parameter, capture) still count, and a from x import * anywhere in the file disqualifies every name in it
  • Dotted annotations (x: mod.Td = {...}) never match, since they cannot name a local class
  • String forward-reference annotations (x: "MyTd") are not recognized

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Note

Low Risk
Lint-only change to an internal checker with conservative guards and broad negative tests; no runtime or auth/data paths touched.

Overview
LIT002 no longer flags dict literals on annotated assignments when they are the same one-shot shape as a TypedDict(...) call. The type-discipline checker adds _typeddict_assigned_value_ids (plus helpers for annotation names and file-wide bindings) and unions that set into the existing LIT002 exemptions alongside annotation internals and freezing-wrapper arguments.

The exemption is narrow: module-top-level TypedDict whose name is bound exactly once, every field (including inherited) is ReadOnly[...], bare or Final[...] annotation only—no dotted types, imported/nested TypedDicts, writable fields, from x import *, or rebound names. Nested mutables inside the literal still trip LIT002. Rule docs and the violation fix message now describe this path; tests cover happy paths and the conservative false-negative guards.

Reviewed by Cursor Bugbot for commit bbb5c94. Bugbot is set up for automated code reviews on this repo. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR exempts annotated dict literals from LIT002 when their annotation resolves conservatively to a top-level, same-module, all-ReadOnly TypedDict.

  • Restricts eligible TypedDict definitions to direct module-level classes.
  • Counts rebinding forms and rejects ambiguous names or files containing star imports.
  • Adds tests for inheritance, writable fields, rebinding, nested definitions, qualified annotations, and nested mutable values.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the current implementation addresses the previously reported scope, rebinding, star-import, and nested-definition cases.

Important Files Changed

Filename Overview
scripts/check_type_discipline.py Implements the conservative TypedDict-literal exemption and now prevents nested definitions from lending file-wide exemptions; no blocking failure remains.
tests/test_litellm/test_check_type_discipline.py Adds focused regression coverage for valid exemptions and the previously reported scope, rebinding, and star-import failure modes.

Reviews (6): Last reviewed commit: "fix(lint): limit the LIT002 exemption to..." | Re-trigger Greptile

Comment thread scripts/check_type_discipline.py Outdated
@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread scripts/check_type_discipline.py
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread scripts/check_type_discipline.py
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread scripts/check_type_discipline.py Outdated
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread scripts/check_type_discipline.py
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit bbb5c94. Configure here.

@mateo-berri

Copy link
Copy Markdown
Contributor Author

Superceded by #36869

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 this pull request may close these issues.

1 participant