Skip to content

Fix #11453: no error on accessing __qualname__ of instance#11503

Open
rchiodo wants to merge 3 commits into
microsoft:mainfrom
rchiodo:fix11453
Open

Fix #11453: no error on accessing __qualname__ of instance#11503
rchiodo wants to merge 3 commits into
microsoft:mainfrom
rchiodo:fix11453

Conversation

@rchiodo

@rchiodo rchiodo commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Description

Pyright/Pylance failed to report the expected "cannot access attribute" diagnostic when __qualname__ was accessed on an instance (e.g. MyClass().__qualname__). At runtime, __qualname__ is exposed via the metaclass type, not as an instance attribute, so instance access raises AttributeError. Class-object access (MyClass.__qualname__) is and should remain valid.

How you figured out what to do

The existing sample classes3.py already documented this exact bug at the instance.__qualname__ line. Reproduced it (no diagnostic emitted), then traced the root cause: the binder unconditionally injects an implicit __qualname__ symbol into each class scope's symbol table (binder.ts ~L510). In a Class scope, _addSymbolToCurrentScope stamps it with SymbolFlags.ClassMember. Instance member-access resolution (getClassMemberIterator) treats any isClassMember() symbol as accessible, so the error was suppressed. Confirmed via debugger that the symbol had isClassMember()=true / isInstanceMember()=false, and via CPython that A().__qualname__ raises AttributeError.

Implementation

Threaded an optional isClassMember parameter (default true) through _addImplicitSymbolToCurrentScope_addSymbolToCurrentScope. The ClassMember flag is now only set when the scope is a Class and isClassMember is true. The __qualname__ injection passes /* isClassMember */ false, so it stays name-resolvable inside the class body (print(__qualname__)) but is no longer exposed as a class/instance member. Class.__qualname__ still resolves via the metaclass type.__qualname__: str. __doc__/__module__ are unchanged (they are legitimately instance-accessible).

Testing

  • Updated classes3.py: instance.__qualname__ now expected error; added reveal_type(..., expected_text="str") guards for class-object access; added subclass, nested/inner class, and type[_T] generic cases. Bumped Classes3 expected error count 3 → 6.
  • Added completions.qualname.fourslash.ts: instance completions exclude __qualname__ (but include __doc__/__module__); class completions include __qualname__.
  • typeEvaluator1-8 + checker (9 suites / 1234 tests) pass; fourslash completions/hover (333 tests) pass.
  • Prettier + ESLint clean on changed files.

Addresses

Fixes https://github.com/microsoft/pylance-release/issues/11453


Generated by fix_all_my_issues pipeline

rchiodo and others added 2 commits June 15, 2026 10:58
The binder injects an implicit __qualname__ symbol into every class scope so
it can be referenced by name within the class body (e.g. print(__qualname__)).
Because the scope is a class scope, the symbol was flagged as a class member,
so member access on an instance resolved it and skipped the expected
attribute-access error. Unlike __doc__/__module__, __qualname__ is exposed via
the metaclass (type), not as a class/instance attribute.

Thread an optional isClassMember flag through the implicit-symbol helpers and
inject __qualname__ without the ClassMember flag. It stays name-resolvable in
the class body, instance access now reports an attribute-access error, and
class-object access still resolves via type.__qualname__: str.

Fixes microsoft#11453

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

@rchiodo

rchiodo commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator Author

It looks to me like this has introduced a whole number of failures along the lines of:

dd-trace-py (https://github.com/DataDog/dd-trace-py)

  • .../projects/dd-trace-py/ddtrace/contrib/internal/asyncio/patch.py:73:32 - error: "qualname" is not a known attribute of "None" (reportOptionalMemberAccess)
  • .../projects/dd-trace-py/ddtrace/vendor/debtcollector/_utils.py:137:52 - error: Cannot access attribute "qualname" for class "MethodType"
  • Attribute "__qualname__" is unknown (reportAttributeAccessIssue)
    
  • 14755 errors, 750 warnings, 0 informations
  • 14757 errors, 750 warnings, 0 informations

pydantic (https://github.com/pydantic/pydantic)

  • .../projects/pydantic/pydantic/_internal/_validate_call.py
  • .../projects/pydantic/pydantic/_internal/_validate_call.py:26:98 - error: Cannot access attribute "qualname" for class "MethodType"
  • Attribute "__qualname__" is unknown (reportAttributeAccessIssue)
    
  • 10 errors, 0 warnings, 0 informations
  • 11 errors, 0 warnings, 0 informations

Inject the implicit __qualname__ symbol after walking the class suite so a
class that explicitly declares __qualname__ (e.g. type, function, and other
typeshed classes) keeps the class-member symbol created for that declaration.

Previously the non-class-member symbol was created up front and the explicit
declaration merged into it without restoring the class-member flag, which hid
the legitimate member and caused reportAttributeAccessIssue errors on valid
instance.__qualname__ accesses (MethodType, function, etc.).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rchiodo

rchiodo commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Good catch — fixed in 73f5461.

Root cause: the implicit qualname symbol was injected into the class scope before the class suite was walked. For typeshed classes that legitimately declare qualname (function, MethodType, type, partial, etc.), that pre-created non-class-member symbol got the explicit declaration merged into it, but _bindNameValueToScope only sets the ClassMember flag when it creates a symbol — so the flag was never restored. The legitimate member was then hidden, producing the new reportAttributeAccessIssue errors on valid instance.__qualname__ accesses across the mypy_primer projects.

Fix: inject the implicit __qualname__ after walking the suite. Now an explicit declaration creates the symbol as a proper class member first, and the implicit injection only adds a non-class-member symbol for classes that don't declare one. Instance access on a plain class still errors (the original #11453 behavior), while function.__qualname__ / MethodType.__qualname__ / explicit __qualname__ members resolve again.

Added regression coverage to classes3.py for a function object's __qualname__ and a class that explicitly declares __qualname__. Classes3 and completions.qualname both pass locally.

@github-actions

Copy link
Copy Markdown
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

sympy (https://github.com/sympy/sympy)
-   .../projects/sympy/sympy/solvers/tests/test_solvers.py:1216:18 - error: Operator "+" not supported for types "Basic" and "Expr" (reportOperatorIssue)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:315:25 - error: "Set" is not iterable
+     "__iter__" method not defined (reportGeneralTypeIssues)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:315:25 - error: "ConditionSet" is not iterable
+     "__iter__" method not defined (reportGeneralTypeIssues)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2030:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2042:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2055:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2076:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
+     Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
+       "Set" is incompatible with protocol "Sized"
+         "__len__" is not present (reportArgumentType)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2077:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2270:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
+     Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
+       "Set" is incompatible with protocol "Sized"
+         "__len__" is not present (reportArgumentType)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2351:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
+     Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
+       "Set" is incompatible with protocol "Sized"
+         "__len__" is not present (reportArgumentType)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2364:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
+     Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
+       "Set" is incompatible with protocol "Sized"
+         "__len__" is not present (reportArgumentType)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2370:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
+     Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
+       "Set" is incompatible with protocol "Sized"
+         "__len__" is not present (reportArgumentType)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2538:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
+     Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
+       "Set" is incompatible with protocol "Sized"
+         "__len__" is not present (reportArgumentType)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2541:20 - error: Argument of type "Unknown | Basic | Any" cannot be assigned to parameter "obj" of type "Sized" in function "len"
+     Type "Unknown | Basic | Any" is not assignable to type "Sized"
+       "Basic" is incompatible with protocol "Sized"
+         "__len__" is not present (reportArgumentType)
-   .../projects/sympy/sympy/stats/tests/test_discrete_rv.py:41:12 - error: Operator "-" not supported for types "Unknown | Expr | tuple[Unknown, ...] | Sum | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | Literal[0]" and "Unknown | Expr | MatrixExpr | One | NegativeOne | Zero | Integer | int"
+   .../projects/sympy/sympy/stats/tests/test_discrete_rv.py:41:12 - error: Operator "-" not supported for types "Unknown | Expr | tuple[Unknown, ...] | Sum | ZeroMatrix | Zero | NaN | Piecewise | Basic | Literal[0]" and "Unknown | Expr | MatrixExpr | One | NegativeOne | Zero | Integer | int"
-   .../projects/sympy/sympy/stats/tests/test_discrete_rv.py:41:37 - error: Operator "**" not supported for types "Unknown | Expr | tuple[Unknown, ...] | Sum | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | Literal[0]" and "Literal[2]"
+   .../projects/sympy/sympy/stats/tests/test_discrete_rv.py:41:37 - error: Operator "**" not supported for types "Unknown | Expr | tuple[Unknown, ...] | Sum | ZeroMatrix | Zero | NaN | Piecewise | Basic | Literal[0]" and "Literal[2]"
-   .../projects/sympy/sympy/stats/tests/test_discrete_rv.py:85:12 - error: Operator "-" not supported for types "Expr | Unknown | tuple[Unknown, ...] | Sum | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | Literal[0]" and "Expr | Unknown | MatrixExpr | One | NegativeOne | Zero | Integer | int"
+   .../projects/sympy/sympy/stats/tests/test_discrete_rv.py:85:12 - error: Operator "-" not supported for types "Expr | Unknown | tuple[Unknown, ...] | Sum | ZeroMatrix | Zero | NaN | Piecewise | Basic | Literal[0]" and "Expr | Unknown | MatrixExpr | One | NegativeOne | Zero | Integer | int"
-   .../projects/sympy/sympy/stats/tests/test_discrete_rv.py:85:37 - error: Operator "**" not supported for types "Expr | Unknown | tuple[Unknown, ...] | Sum | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | Literal[0]" and "Literal[2]"
+   .../projects/sympy/sympy/stats/tests/test_discrete_rv.py:85:37 - error: Operator "**" not supported for types "Expr | Unknown | tuple[Unknown, ...] | Sum | ZeroMatrix | Zero | NaN | Piecewise | Basic | Literal[0]" and "Literal[2]"
-   .../projects/sympy/sympy/stats/tests/test_discrete_rv.py:138:33 - error: Operator "+" not supported for types "Expr | Basic | Expectation | tuple[Unknown, ...] | Unknown | Sum | ZeroMatrix | Add | Zero | NaN | Piecewise | Integral | Any | ExpectationMatrix | Literal[0]" and "Expr | Unknown | tuple[Unknown, ...] | MatMul | One | NegativeOne | Zero | Integer | NaN | ComplexInfinity | Rational | Any | int"
+   .../projects/sympy/sympy/stats/tests/test_discrete_rv.py:138:33 - error: Operator "+" not supported for types "Expr | Basic | Expectation | tuple[Unknown, ...] | Unknown | Sum | ZeroMatrix | Zero | NaN | Piecewise | Integral | Any | ExpectationMatrix | Literal[0]" and "Expr | Unknown | tuple[Unknown, ...] | MatMul | One | NegativeOne | Zero | Integer | NaN | ComplexInfinity | Rational | Any | int"
-   .../projects/sympy/sympy/stats/tests/test_discrete_rv.py:138:33 - error: Operator "+" not supported for types "Expr | Unknown | Any | One | NegativeOne | Zero | Integer | NaN | ComplexInfinity | Rational | tuple[Unknown, ...] | MatAdd | int" and "Literal[3]"
+   .../projects/sympy/sympy/stats/tests/test_discrete_rv.py:138:33 - error: Operator "+" not supported for types "Expr | Unknown | Any | One | NegativeOne | Zero | Integer | NaN | ComplexInfinity | Rational | tuple[Unknown, ...] | MatAdd | Infinity | NegativeInfinity | Float | Number | int" and "Literal[3]"
-   .../projects/sympy/sympy/stats/tests/test_discrete_rv.py:138:43 - error: Operator "*" not supported for types "Literal[2]" and "RandomSymbol | Basic | Expectation | Expr | tuple[Unknown, ...] | Unknown | Sum | ZeroMatrix | Add | Zero | NaN | Piecewise | Integral | Any | ExpectationMatrix | Literal[0]"
+   .../projects/sympy/sympy/stats/tests/test_discrete_rv.py:138:43 - error: Operator "*" not supported for types "Literal[2]" and "RandomSymbol | Basic | Expectation | Expr | tuple[Unknown, ...] | Unknown | Sum | ZeroMatrix | Zero | NaN | Piecewise | Integral | Any | ExpectationMatrix | Literal[0]"
-   .../projects/sympy/sympy/stats/tests/test_discrete_rv.py:164:21 - error: Argument of type "RandomSymbol | Basic | Expectation | Expr | tuple[Unknown, ...] | Unknown | Sum | ZeroMatrix | Add | Zero | NaN | Piecewise | Integral | Any | ExpectationMatrix | Literal[0]" cannot be assigned to parameter "expr" of type "Basic" in function "simplify"
+   .../projects/sympy/sympy/stats/tests/test_discrete_rv.py:164:21 - error: Argument of type "RandomSymbol | Basic | Expectation | Expr | tuple[Unknown, ...] | Unknown | Sum | ZeroMatrix | Zero | NaN | Piecewise | Integral | Any | ExpectationMatrix | Literal[0]" cannot be assigned to parameter "expr" of type "Basic" in function "simplify"
-     Type "RandomSymbol | Basic | Expectation | Expr | tuple[Unknown, ...] | Unknown | Sum | ZeroMatrix | Add | Zero | NaN | Piecewise | Integral | Any | ExpectationMatrix | Literal[0]" is not assignable to type "Basic"
+     Type "RandomSymbol | Basic | Expectation | Expr | tuple[Unknown, ...] | Unknown | Sum | ZeroMatrix | Zero | NaN | Piecewise | Integral | Any | ExpectationMatrix | Literal[0]" is not assignable to type "Basic"

... (truncated 329 lines) ...

streamlit (https://github.com/streamlit/streamlit)
+   .../projects/streamlit/lib/streamlit/type_util.py:131:56 - error: Cannot access attribute "__qualname__" for class "object"
+     Attribute "__qualname__" is unknown (reportAttributeAccessIssue)
+   .../projects/streamlit/lib/streamlit/type_util.py:132:20 - error: Cannot access attribute "__qualname__" for class "object"
+     Attribute "__qualname__" is unknown (reportAttributeAccessIssue)
- 5595 errors, 111 warnings, 0 informations
+ 5597 errors, 111 warnings, 0 informations

dd-trace-py (https://github.com/DataDog/dd-trace-py)
+   .../projects/dd-trace-py/ddtrace/contrib/internal/asyncio/patch.py:73:32 - error: "__qualname__" is not a known attribute of "None" (reportOptionalMemberAccess)
- 14915 errors, 754 warnings, 0 informations
+ 14916 errors, 754 warnings, 0 informations

trio (https://github.com/python-trio/trio)
-   .../projects/trio/src/trio/_core/_run.py:1974:68 - error: Unnecessary "# type: ignore" comment (reportUnnecessaryTypeIgnoreComment)
+   .../projects/trio/src/trio/_deprecate.py:51:38 - error: Type of "__qualname__" is unknown (reportUnknownMemberType)
+   .../projects/trio/src/trio/_deprecate.py:51:44 - error: Cannot access attribute "__qualname__" for class "object"
+     Attribute "__qualname__" is unknown (reportAttributeAccessIssue)
+   .../projects/trio/src/trio/_util.py:221:25 - error: Cannot assign to attribute "__qualname__" for class "object"
+     Attribute "__qualname__" is unknown (reportAttributeAccessIssue)
- 1686 errors, 13 warnings, 0 informations
+ 1688 errors, 13 warnings, 0 informations

spack (https://github.com/spack/spack)
+   .../projects/spack/lib/spack/spack/build_environment.py:1517:37 - error: Cannot access attribute "__qualname__" for class "str"
+     Attribute "__qualname__" is unknown (reportAttributeAccessIssue)
- 2146 errors, 26 warnings, 0 informations
+ 2147 errors, 26 warnings, 0 informations

jax (https://github.com/google/jax)
+   .../projects/jax/jax/_src/api.py:1972:51 - error: Cannot access attribute "__qualname__" for class "partial[Unknown]"
+     Attribute "__qualname__" is unknown (reportAttributeAccessIssue)
+   .../projects/jax/jax/_src/util.py:537:11 - error: Cannot assign to attribute "__qualname__" for class "object*"
+     Attribute "__qualname__" is unknown (reportAttributeAccessIssue)
- 3403 errors, 90 warnings, 0 informations
+ 3405 errors, 90 warnings, 0 informations

@rchiodo

rchiodo commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up on the mypy_primer __qualname__ diff (the run posted after 73f5461 still lists __qualname__ errors, but they're now a different, expected set):

  • Fixed regressions (false positives that hid legitimate members): the MethodType / function / partial-via-_lru_cache_wrapper etc. cases from the original report are gone. These typeshed classes explicitly declare __qualname__: str, so instance.__qualname__ resolves again. Verified: Classes3 and completions.qualname pass, and dd-trace-py/pydantic no longer show the MethodType.__qualname__ error.

  • Remaining __qualname__ diffs are intended true positives of no error on accessing __qualname__ of instance #11453: object, str, None, and functools.partial do not declare __qualname__ as a class/instance attribute (it lives on the metaclass type). At runtime object().__qualname__, "x".__qualname__, and partial(f).__qualname__ all raise AttributeError, so flagging them (reportAttributeAccessIssue / reportOptionalMemberAccess) is the correct new behavior — these are pre-existing latent bugs in jax/streamlit/spack/dd-trace-py that the fix now surfaces.

Mechanism of the fix (73f5461): the implicit __qualname__ symbol is now injected after walking the class suite. A class that explicitly declares __qualname__ therefore creates the class-member symbol first; the implicit injection (with isClassMember=false) finds the existing symbol and leaves its ClassMember flag intact. Plain classes that don't declare it get a non-class-member symbol, so instance access still errors as intended.

# This should not generate an error because the attribute is explicitly declared
# as a class member.
WithQualname().__qualname__
reveal_type(WithQualname().__qualname__, expected_text="str")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The WithQualname block asserts WithQualname().__qualname__ is valid str, but at runtime type.__new__ strips a class-body __qualname__ assignment from __dict__, so WithQualname().__qualname__ raises AttributeError. This is consistent with pyright's general modeling of class-body assignments as instance-accessible class variables, so changing the diagnostic may be out of scope — but the inline comment "exposes it on instances" is factually inaccurate and slightly contradicts this PR's stated principle. Please reword the comment to note the runtime would AttributeError (and that this reflects pyright's class-variable modeling), so the test doesn't read as enshrining runtime behavior it doesn't match.

// and the explicit declaration would merge into it without restoring the
// class-member flag, incorrectly hiding the legitimate member.
this._addImplicitSymbolToCurrentScope('__qualname__', node, 'str', /* isClassMember */ false);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Moving the __qualname__ injection to after this.walk(node.d.suite) means that for symbols with a real declaration (typeshed function/type, or a user __qualname__ = "..."), the synthetic empty-range Intrinsic declaration is now appended last via addDeclaration. Consumers that select the "last" declaration for hover / go-to-definition could now resolve to the empty range instead of the real declaration. reveal_type still yields str, so existing tests won't catch this. Worth verifying "Go to Definition" on func1.__qualname__ lands on the real declaration in a post-patch build.

Worktree: `C:\Users\rchiodo\.fix_loop\repos\microsoft__pyright\.worktrees_8765\fix11453`

## Context

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Repo/verb mismatch: the plan and pyright-internal test placement treat this as microsoft/pyright issue #11453 (matching the "Fixes" verb), but the PR footer points at microsoft/pylance-release. Per repo convention, pylance-release issues use "Addresses" and only pyrx issues use "Fixes". Please reconcile the repo and verb before merge.

@heejaechang

Copy link
Copy Markdown
Collaborator

Solid, surgical fix for instance __qualname__ access. A couple of test/metadata cleanups and one navigation behavior worth a quick verification, but nothing blocking.

@heejaechang heejaechang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved via Review Center.

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.

2 participants