Skip to content

fix(python): avoid TypeVar collisions with generated class names - #4307

Open
Vaibhav701161 wants to merge 1 commit into
BoundaryML:canaryfrom
Vaibhav701161:fix/python-typevar-class-name-collision
Open

fix(python): avoid TypeVar collisions with generated class names#4307
Vaibhav701161 wants to merge 1 commit into
BoundaryML:canaryfrom
Vaibhav701161:fix/python-typevar-class-name-collision

Conversation

@Vaibhav701161

@Vaibhav701161 Vaibhav701161 commented Jul 31, 2026

Copy link
Copy Markdown

Closes #4083.

The bug

A generic parameter and an ordinary definition can share a name. When they do, the generated Python Pydantic v2 leaf declared the TypeVar first and then rebound the same identifier as a class, so every generic reference in the file resolved to the class instead of the parameter:

class T {
  label string
}

class Box<T> {
  item T
}

function echo<T>(value: T) -> T { ... }
T = typing.TypeVar("T")


class T(pydantic.BaseModel):          # rebinds T; the TypeVar is unreachable
    label: str


class Box(pydantic.BaseModel, typing.Generic[T]):   # T is the model now
    item: T

This is not cosmetic - the generated SDK does not import at all:

TypeError: Parameters to Generic[...] must all be type variables
           or parameter specification variables.

Root cause

Two layers had to agree, and neither did.

1. Name resolution (baml_compiler2_tir). lower_path only consulted resolve_type_var inside its Err fallback - i.e. only once concrete-type resolution had already failed. With a class T in the same package that lookup succeeds, so the T in function echo<T>(value: T) lowered to Ty::Class, not Ty::TypeVar. The generic parameter was being captured by the class before codegen ever ran.

2. Binding allocation (sdkgen_python_pydantic2). Leaf TypeVars were emitted under their source spelling with nothing checking it against the rest of the module namespace.

The change

  • lower_path now resolves a bare, unparameterized in-scope generic parameter before resolve_type, so the parameter shadows a same-named concrete type. Qualified paths (pkg.T) still reach the class through the namespace, and a parameterized spelling (T<int>) still falls through to the existing path.
  • New LeafBody::allocated_typevars() maps each BAML generic name to a collision-free Python identifier, appending _ until the name is free. The reserved set is the leaf's own symbols, its cross-leaf import anchors, and MODULE_BINDINGS — the stdlib / baml_bridge / builtins names a leaf can bind on its own, so a parameter named typing can't rebind the typing import either.
  • TranslateCtx carries that map so declarations and every reference render from the same allocation. .py and .pyi compute the reserved set identically - it is deliberately independent of which file is being rendered — so the two files can never disagree on a spelling.

Runtime-facing names are untouched: the binding is still typing.TypeVar("T") and type_params=["T"] still carries the BAML spelling. Only the local Python identifier moves, and only when it would otherwise collide.

-T = typing.TypeVar("T")
+T_ = typing.TypeVar("T")


 class T(pydantic.BaseModel):
     label: str


-class Box(pydantic.BaseModel, typing.Generic[T]):
-    item: T
+class Box(pydantic.BaseModel, typing.Generic[T_]):
+    item: T_

Tests

  • typevar_binding_avoids_same_leaf_symbol_name - the issue's scenario end to end; asserts the class keeps the plain name, .py and .pyi both follow the allocated one, and type_params stays the BAML spelling.
  • typevar_binding_escapes_chained_and_import_collisions - allocation walks a chain (T and T_ both taken, so the parameter lands on T__) and escapes an import name no symbol owns (a parameter named typing becomes typing_).
  • test_bare_generic_param_shadows_same_named_class_in_codegen - covers the resolution half: with class T and function echo<T> in one file, the argument and return types lower to Ty::TypeVar, with no diagnostics.

Both scenarios were also checked by importing the generated package under real Pydantic v2. The pre-fix output raises TypeError on import; the post-fix output imports cleanly and keeps the BAML spelling at runtime:

scenario parameters after import instantiation
class T + Box<T> (~T,) Box[int](item=3)
T/T_ taken, params T + typing (~T, ~typing) Box[int, str](item=1, tag='x')

Note on scope

lower_type_expr.rs and client_codegen.rs sit outside the issue's stated affected area. They are in scope because renaming alone would not have fixed anything: the type checker was handing codegen a Ty::Class where the schema said Ty::TypeVar, so the wrong entity was being referenced regardless of what it was called.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed generic type parameters being incorrectly shadowed by same-named concrete types.
    • Improved generated Python typing when generic names conflict with imports, symbols, or other type variables.
    • Ensured generic classes, aliases, callbacks, and annotations consistently reference their allocated type-variable names.
  • Tests

    • Added coverage for generic shadowing and type-variable name collisions in generated .py and .pyi files.

@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

@Vaibhav701161 is attempting to deploy a commit to the Boundary Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The compiler now resolves bare generic parameters before same-named concrete types. The Python Pydantic v2 generator allocates unique TypeVar identifiers and propagates them through runtime code, stubs, annotations, aliases, and protocol rendering.

Changes

Generic type resolution and Python TypeVar rendering

Layer / File(s) Summary
Compiler generic resolution
baml_language/crates/baml_compiler2_tir/src/lower_type_expr.rs, baml_language/crates/baml_project/src/client_codegen.rs
Bare in-scope generic parameters now take precedence over same-named concrete types. Regression coverage verifies generated argument and return types use the generic parameter.
Collision-free TypeVar allocation and translation context
baml_language/sdks/python/rust/sdkgen_python_pydantic2/src/leaf.rs, baml_language/sdks/python/rust/sdkgen_python_pydantic2/src/translate_ty.rs
The generator reserves module bindings and maps source generic names to unique emitted identifiers. Type translation uses this mapping when available.
Runtime and stub rendering
baml_language/sdks/python/rust/sdkgen_python_pydantic2/src/leaf.rs
Allocated names now apply to .py and .pyi class bases, annotations, aliases, symbols, callback protocols, and callable-child protocols. Runtime TypeVar arguments retain source names.
Collision regression tests
baml_language/sdks/python/rust/sdkgen_python_pydantic2/src/lib.rs
Tests cover collisions with symbols, previously allocated names, and imports in generated .py and .pyi files.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BAMLCompiler
  participant LeafBody
  participant TranslateCtx
  participant GeneratedPython
  BAMLCompiler->>BAMLCompiler: resolve bare generic parameter before concrete type
  BAMLCompiler->>LeafBody: provide generic source names
  LeafBody->>LeafBody: allocate collision-free identifiers
  LeafBody->>TranslateCtx: pass TypeVar mapping
  TranslateCtx->>GeneratedPython: render mapped annotations and bases
  LeafBody->>GeneratedPython: emit TypeVar declarations with source names
Loading

Possibly related PRs

Suggested reviewers: sxlijin

Poem

A rabbit finds a TypeVar name,
And keeps each binding clear,
T_ hops past a class called T,
While T stays true and dear.
Python and stubs match their steps.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: preventing Python TypeVar name collisions with generated class names.
Linked Issues check ✅ Passed The changes satisfy [#4083] by allocating collision-free TypeVar names and using them consistently in generated .py and .pyi output with regression tests.
Out of Scope Changes check ✅ Passed All changes support [#4083], including generic resolution, TypeVar allocation, translated references, generated output, and regression coverage.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Vaibhav701161
Vaibhav701161 marked this pull request as ready for review July 31, 2026 18:10

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
baml_language/sdks/python/rust/sdkgen_python_pydantic2/src/translate_ty.rs (1)

94-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a direct unit test for the typevars substitution path.

The substitution logic here is correct, but no Case in this file's translate_ty_covers_phase_g3_matrix exercises a populated ctx.typevars map. Coverage for the substitution behavior currently comes only from full-pipeline tests in lib.rs (typevar_binding_avoids_same_leaf_symbol_name, typevar_binding_escapes_chained_and_import_collisions).

Add a Case that builds a ctx with typevars: Some(Rc::new(map)) containing a "T" -> "T_" entry, asserting translate_ty emits "T_", plus a case where the map is present but lacks the key, asserting the fallback to the raw source name. This keeps the substitution behavior covered at the unit level, matching this file's existing test style.

As per path instructions, "**/*.rs: Prefer writing Rust unit tests over integration tests where possible."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/sdks/python/rust/sdkgen_python_pydantic2/src/translate_ty.rs`
around lines 94 - 99, Add direct unit-test cases to
translate_ty_covers_phase_g3_matrix for Ty::TypeVar: one with ctx.typevars
containing “T” mapped to “T_” and asserting the substituted output, and another
with a present map missing the key and asserting the raw source-name fallback.
Follow the existing Case/test style and construct the context using the file’s
established Rc map setup.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@baml_language/sdks/python/rust/sdkgen_python_pydantic2/src/translate_ty.rs`:
- Around line 94-99: Add direct unit-test cases to
translate_ty_covers_phase_g3_matrix for Ty::TypeVar: one with ctx.typevars
containing “T” mapped to “T_” and asserting the substituted output, and another
with a present map missing the key and asserting the raw source-name fallback.
Follow the existing Case/test style and construct the context using the file’s
established Rc map setup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dc2c385b-c4f8-4ce5-8c0c-ca88f279b1a8

📥 Commits

Reviewing files that changed from the base of the PR and between e26ee02 and a635a0f.

📒 Files selected for processing (5)
  • baml_language/crates/baml_compiler2_tir/src/lower_type_expr.rs
  • baml_language/crates/baml_project/src/client_codegen.rs
  • baml_language/sdks/python/rust/sdkgen_python_pydantic2/src/leaf.rs
  • baml_language/sdks/python/rust/sdkgen_python_pydantic2/src/lib.rs
  • baml_language/sdks/python/rust/sdkgen_python_pydantic2/src/translate_ty.rs

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.

Python Pydantic v2: avoid ordinary class and module TypeVar name shadowing

1 participant