Skip to content

Preserve the class type through proxy_for_class (#418) - #502

Merged
pschanely merged 1 commit into
pschanely:mainfrom
rahul188:fix-418-proxy-for-class-typevar
Aug 12, 2026
Merged

Preserve the class type through proxy_for_class (#418)#502
pschanely merged 1 commit into
pschanely:mainfrom
rahul188:fix-418-proxy-for-class-typevar

Conversation

@rahul188

Copy link
Copy Markdown
Contributor

Fixes #418.

proxy_for_class was annotated (typ: Type, varname: str) -> object, so callers got back something with no attributes and had to cast or silence mypy to use it.

The change

-def proxy_for_class(typ: Type, varname: str) -> object:
+def proxy_for_class(typ: Type[_T], varname: str) -> _T:

reusing the _T already defined in core.py. This is a pure annotation change — no runtime behavior differs.

The annotation matches what the function already does: the main path returns WithEnforcement(cls)(*args.args, **args.kwargs), an instance of the requested class. The parameterized-generic case works out too, since mypy types Container[int] in expression position as type[Container[int]], so _T binds to Container[int] and the result keeps its parameter.

The one place needing help is the TypedDict branch, which returns a dict comprehension. A TypedDict instance is a dict at runtime, so the value genuinely satisfies the declared return type, but mypy can't connect that through _T. I used a cast with a comment there rather than widening the signature back out — the alternative would give up the improvement for every other class to accommodate one branch.

What pins it

core_test.py carried three # type: ignore[attr-defined] comments — the exact workaround the issue describes:

assert isinstance(obj.value, SymbolicInt)   # type: ignore[attr-defined]
assert isinstance(obj.first, SymbolicInt)   # type: ignore[attr-defined]
assert isinstance(obj.second, LazyIntSymbolicStr)  # type: ignore[attr-defined]

They are removed. Since mypy runs over all Python files via the pre-commit hook, those lines now act as the regression check for this signature — verified by reverting core.py and leaving the tests as-is:

crosshair/core_test.py:867: error: "object" has no attribute "value"  [attr-defined]
crosshair/core_test.py:882: error: "object" has no attribute "first"  [attr-defined]
crosshair/core_test.py:883: error: "object" has no attribute "second"  [attr-defined]

With the change, mypy --ignore-missing-imports --scripts-are-modules crosshair/core.py crosshair/core_test.py reports Success: no issues found.

Testing

  • crosshair/core_test.py: 95 passed, 3 skipped.
  • black --check and isort --check-only clean on both files.
  • flake8 reports the same three pre-existing F824 warnings in core.py before and after; nothing new.

proxy_for_class was annotated as returning `object`, so callers lost
every attribute of the class they asked for and had to reach for a
cast or a `# type: ignore` to touch the result.

Bind the argument and the return with the existing _T type variable.
The main path already returns an instance of the requested class
(WithEnforcement(cls)(...)), so the annotation describes what the
function was doing all along.

The TypedDict branch returns a plain dict, which is what a TypedDict
instance is at runtime; mypy cannot connect that to _T on its own, so
it takes a cast with a comment rather than widening the signature back
out.

Three `# type: ignore[attr-defined]` comments in core_test.py --
exactly the workaround the issue reports -- are no longer needed and
are removed, which is what pins the improvement: without the signature
change mypy fails on those three lines.
@pschanely

Copy link
Copy Markdown
Owner

Lovely. Thank you!

@pschanely
pschanely merged commit 9cb0083 into pschanely:main Aug 12, 2026
15 checks passed
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.

proxy_for_class loses type information by returning object

2 participants