Skip to content

fix urlunparse returns Literal[b''] instead of str #2867#3003

Open
asukaminato0721 wants to merge 1 commit intofacebook:mainfrom
asukaminato0721:2867
Open

fix urlunparse returns Literal[b''] instead of str #2867#3003
asukaminato0721 wants to merge 1 commit intofacebook:mainfrom
asukaminato0721:2867

Conversation

@asukaminato0721
Copy link
Copy Markdown
Contributor

@asukaminato0721 asukaminato0721 commented Apr 4, 2026

Summary

Fixes #2867

changed as_superclass so tuple-like NamedTuple subclasses are upcast through their erased tuple element types instead of the old tuple[Any, ...] fallback.

stops ParseResult from matching Iterable[None], which was why urlunparse incorrectly chose the Literal[b""] overload.

Test Plan

add test

@meta-cla meta-cla bot added the cla signed label Apr 4, 2026
@asukaminato0721 asukaminato0721 changed the title fix urlunparse returns Literal[b''] instead of str #286 fix urlunparse returns Literal[b''] instead of str #2867 Apr 4, 2026
@github-actions github-actions bot added the size/m label Apr 4, 2026
@asukaminato0721 asukaminato0721 marked this pull request as ready for review April 4, 2026 11:37
Copilot AI review requested due to automatic review settings April 4, 2026 11:37
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates Pyrefly’s handling of stdlib NamedTuple-like result types (e.g., urllib.parse.ParseResult) so overload resolution and collection subtyping behave correctly, fixing the urlunparse return-type false positive reported in #2867.

Changes:

  • Adds a regression test asserting urlunparse(urlparse(str)) and urlunparse(ParseResult._replace(...)) are typed as str.
  • Allows NamedTuple multiple inheritance in stub/interface (.pyi) modules to match typeshed modeling of stdlib result objects.
  • Improves superclass lookups for NamedTuple subclasses by routing tuple-related upcasts through an erased tuple type so Iterable/Sequence element types reflect actual field types instead of Any.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
pyrefly/lib/test/overload.rs Adds regression coverage for urlunparse preferring the str overload when given a ParseResult.
pyrefly/lib/test/named_tuple.rs Strengthens NamedTuple subtyping tests and adds a stub-mixin scenario reflecting typeshed patterns.
pyrefly/lib/alt/class/classdef.rs Adjusts as_superclass to use tuple element information (via as_tuple + tuple erasure) for tuple-related supertypes.
pyrefly/lib/alt/class/class_metadata.rs Permits NamedTuple multiple inheritance in .pyi/interface modules while keeping the restriction for user .py code.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

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

pip (https://github.com/pypa/pip)
- ERROR src/pip/_internal/models/link.py:181:11-34: `+` is not supported between `str` and `bytes` [unsupported-operation]

openlibrary (https://github.com/internetarchive/openlibrary)
- ERROR openlibrary/coverstore/utils.py:92:12-19: Returned type `tuple[Literal[b''], dict[str, str]]` is not assignable to declared return type `tuple[str, dict[str, str]]` [bad-return]
- ERROR openlibrary/plugins/upstream/utils.py:1544:20-23: Returned type `Literal[b'']` is not assignable to declared return type `str` [bad-return]

mkdocs (https://github.com/mkdocs/mkdocs)
- ERROR mkdocs/config/config_options.py:524:20-42: Returned type `Literal[b'']` is not assignable to declared return type `str` [bad-return]

black (https://github.com/psf/black)
+ ERROR src/black/cache.py:144:59-146:18: `dict[str, tuple[float | int | str, ...]]` is not assignable to `dict[str, tuple[float, int, str]]` [bad-assignment]

cloud-init (https://github.com/canonical/cloud-init)
- ERROR cloudinit/url_helper.py:513:26-40: `str | Unknown` is not assignable to TypedDict key `method` with type `Literal[b''] | bool` [bad-typed-dict-key]
- ERROR cloudinit/url_helper.py:516:35-42: `tuple[Unknown, ...]` is not assignable to TypedDict key `timeout` with type `Literal[b''] | bool` [bad-typed-dict-key]
+ ERROR cloudinit/url_helper.py:516:35-42: `tuple[Unknown, ...]` is not assignable to TypedDict key `timeout` with type `bool | str` [bad-typed-dict-key]
- ERROR cloudinit/url_helper.py:518:35-57: `float` is not assignable to TypedDict key `timeout` with type `Literal[b''] | bool` [bad-typed-dict-key]
+ ERROR cloudinit/url_helper.py:518:35-57: `float` is not assignable to TypedDict key `timeout` with type `bool | str` [bad-typed-dict-key]
- ERROR cloudinit/url_helper.py:548:31-38: `dict[Unknown, Unknown] | Unknown` is not assignable to TypedDict key `headers` with type `Literal[b''] | bool` [bad-typed-dict-key]
+ ERROR cloudinit/url_helper.py:548:31-38: `dict[Unknown, Unknown] | Unknown` is not assignable to TypedDict key `headers` with type `bool | str` [bad-typed-dict-key]
- Object of class `bytes` has no attribute `get` [missing-attribute]
+ Object of class `str` has no attribute `get` [missing-attribute]
- ERROR cloudinit/url_helper.py:558:25-50: Cannot set item in `Literal[b'']` [unsupported-operation]
+ ERROR cloudinit/url_helper.py:558:25-50: Cannot set item in `str` [unsupported-operation]
- ERROR cloudinit/url_helper.py:590:35-38: Argument `Literal[b'']` is not assignable to parameter `url` with type `str | None` in function `UrlError.__init__` [bad-argument-type]
- ERROR cloudinit/url_helper.py:596:21-24: Argument `Literal[b'']` is not assignable to parameter `url` with type `str | None` in function `UrlError.__init__` [bad-argument-type]
- ERROR cloudinit/url_helper.py:600:41-44: Argument `Literal[b'']` is not assignable to parameter `url` with type `str | None` in function `UrlError.__init__` [bad-argument-type]

dulwich (https://github.com/dulwich/dulwich)
- ERROR dulwich/client.py:4990:17-40: Argument `Literal[b'']` is not assignable to parameter `base_url` with type `str` in function `Urllib3HttpGitClient.__init__` [bad-argument-type]
- ERROR dulwich/client.py:5004:17-40: Argument `Literal[b'']` is not assignable to parameter `base_url` with type `str` in function `AbstractHttpGitClient.__init__` [bad-argument-type]
- ERROR dulwich/client.py:5016:37-58: `Literal[b'']` is not assignable to attribute `_url_with_auth` with type `str | None` [bad-assignment]

yarl (https://github.com/aio-libs/yarl)
+ ERROR yarl/_url.py:570:30-54: `SplitResult` is not assignable to upper bound `tuple[_T_co, ...]` of type variable `Self@tuple` [bad-specialization]

twine (https://github.com/pypa/twine)
- ERROR twine/utils.py:238:12-30: Returned type `Literal[b'']` is not assignable to declared return type `str` [bad-return]

schemathesis (https://github.com/schemathesis/schemathesis)
- ERROR src/schemathesis/core/output/sanitization.py:74:12-43: Returned type `Literal[b'']` is not assignable to declared return type `str` [bad-return]
- ERROR src/schemathesis/transport/prepare.py:80:16-33: Returned type `Literal[b'']` is not assignable to declared return type `str` [bad-return]

aiohttp (https://github.com/aio-libs/aiohttp)
+ ERROR aiohttp/_websocket/reader_c.py:293:36-87: `WSMessageText` is not assignable to upper bound `tuple[_T_co, ...]` of type variable `Self@tuple` [bad-specialization]
+ ERROR aiohttp/_websocket/reader_c.py:296:36-298:22: `WSMessageTextBytes` is not assignable to upper bound `tuple[_T_co, ...]` of type variable `Self@tuple` [bad-specialization]
+ ERROR aiohttp/_websocket/reader_c.py:300:32-302:18: `WSMessageBinary` is not assignable to upper bound `tuple[_T_co, ...]` of type variable `Self@tuple` [bad-specialization]
+ ERROR aiohttp/_websocket/reader_py.py:293:36-87: `WSMessageText` is not assignable to upper bound `tuple[_T_co, ...]` of type variable `Self@tuple` [bad-specialization]
+ ERROR aiohttp/_websocket/reader_py.py:296:36-298:22: `WSMessageTextBytes` is not assignable to upper bound `tuple[_T_co, ...]` of type variable `Self@tuple` [bad-specialization]
+ ERROR aiohttp/_websocket/reader_py.py:300:32-302:18: `WSMessageBinary` is not assignable to upper bound `tuple[_T_co, ...]` of type variable `Self@tuple` [bad-specialization]
+ ERROR aiohttp/client_reqrep.py:122:29-124:10: `Self@RequestInfo` is not assignable to upper bound `tuple[_T_co, ...]` of type variable `Self@tuple` [bad-specialization]
+ ERROR aiohttp/client_reqrep.py:337:29-339:10: `RequestInfo` is not assignable to upper bound `tuple[_T_co, ...]` of type variable `Self@tuple` [bad-specialization]
+ ERROR aiohttp/client_reqrep.py:784:29-795:10: `ConnectionKey` is not assignable to upper bound `tuple[_T_co, ...]` of type variable `Self@tuple` [bad-specialization]
+ ERROR aiohttp/client_reqrep.py:1043:29-1054:10: `ConnectionKey` is not assignable to upper bound `tuple[_T_co, ...]` of type variable `Self@tuple` [bad-specialization]

streamlit (https://github.com/streamlit/streamlit)
- ERROR lib/tests/streamlit/web/server/server_test_case.py:89:13-47: Argument `Literal[b'']` is not assignable to parameter `url` with type `HTTPRequest | str` in function `tornado.websocket.websocket_connect` [bad-argument-type]

zulip (https://github.com/zulip/zulip)
- ERROR corporate/views/remote_billing_page.py:527:12-21: Returned type `Literal[b'']` is not assignable to declared return type `str` [bad-return]

static-frame (https://github.com/static-frame/static-frame)
- ERROR static_frame/core/www.py:115:16-74: Returned type `Literal[b'']` is not assignable to declared return type `str` [bad-return]

mypy (https://github.com/python/mypy)
- ERROR mypy/typeshed/stdlib/ssl.pyi:263:7-14: Named tuples do not support multiple inheritance [invalid-inheritance]
- ERROR mypy/typeshed/stdlib/urllib/parse.pyi:87:7-19: Named tuples do not support multiple inheritance [invalid-inheritance]
- ERROR mypy/typeshed/stdlib/urllib/parse.pyi:90:7-18: Named tuples do not support multiple inheritance [invalid-inheritance]
- ERROR mypy/typeshed/stdlib/urllib/parse.pyi:93:7-18: Named tuples do not support multiple inheritance [invalid-inheritance]
- ERROR mypy/typeshed/stdlib/urllib/parse.pyi:97:7-24: Named tuples do not support multiple inheritance [invalid-inheritance]
- ERROR mypy/typeshed/stdlib/urllib/parse.pyi:100:7-23: Named tuples do not support multiple inheritance [invalid-inheritance]
- ERROR mypy/typeshed/stdlib/urllib/parse.pyi:103:7-23: Named tuples do not support multiple inheritance [invalid-inheritance]

meson (https://github.com/mesonbuild/meson)
- ERROR mesonbuild/cargo/interpreter.py:888:11-84: `Literal[b'']` is not assignable to variable `url` with type `str` [bad-assignment]
- ERROR mesonbuild/wrap/wrap.py:120:38-66: Argument `Literal[b'']` is not assignable to parameter `url` with type `str` in function `urllib.request.Request.__init__` [bad-argument-type]
- ERROR mesonbuild/wrap/wrap.py:815:33-113: `Literal[b'']` is not assignable to variable `urlstring` with type `str` [bad-assignment]

sphinx (https://github.com/sphinx-doc/sphinx)
- ERROR sphinx/builders/linkcheck.py:780:20-66: Returned type `Literal[b'']` is not assignable to declared return type `str | None` [bad-return]
- ERROR sphinx/ext/intersphinx/_load.py:461:16-33: Returned type `Literal[b'']` is not assignable to declared return type `str` [bad-return]
- ERROR sphinx/ext/intersphinx/_load.py:482:12-29: Returned type `Literal[b'']` is not assignable to declared return type `str` [bad-return]

PyGithub (https://github.com/PyGithub/PyGithub)
- ERROR github/Requester.py:551:16-34: Returned type `Literal[b'']` is not assignable to declared return type `str` [bad-return]

spack (https://github.com/spack/spack)
- ERROR lib/spack/spack/cmd/ci.py:727:12-84: Returned type `Literal[b'']` is not assignable to declared return type `str` [bad-return]
- ERROR lib/spack/spack/oci/oci.py:47:12-49:6: Returned type `Literal[b'']` is not assignable to declared return type `str` [bad-return]
- ERROR lib/spack/spack/oci/opener.py:294:27-58: Argument `Literal[b'']` is not assignable to parameter `url` with type `str` in function `urllib.request.Request.__init__` [bad-argument-type]
- ERROR lib/spack/spack/util/url.py:107:36-40: Argument `Literal[b'']` is not assignable to parameter `filename` with type `str` in function `spack.util.path.sanitize_filename` [bad-argument-type]

poetry (https://github.com/python-poetry/poetry)
- ERROR tests/integration/test_utils_vcs_git.py:438:18-39: Object of class `bytes` has no attribute `encode` [missing-attribute]
- ERROR src/poetry/vcs/git/backend.py:606:12-27: Returned type `Literal[b'']` is not assignable to declared return type `str` [bad-return]

scrapy (https://github.com/scrapy/scrapy)
- ERROR scrapy/http/request/form.py:62:21-82: Argument `Literal[b'']` is not assignable to parameter `url` with type `str` in function `scrapy.http.request.Request._set_url` [bad-argument-type]

prefect (https://github.com/PrefectHQ/prefect)
- ERROR src/integrations/prefect-azure/prefect_azure/repository.py:122:16-24: Returned type `Literal[b''] | str` is not assignable to declared return type `str` [bad-return]
- ERROR src/integrations/prefect-bitbucket/prefect_bitbucket/credentials.py:152:16-84: Returned type `Literal[b'']` is not assignable to declared return type `str` [bad-return]
- ERROR src/integrations/prefect-bitbucket/prefect_bitbucket/repository.py:137:16-24: Returned type `Literal[b''] | str` is not assignable to declared return type `str` [bad-return]
- ERROR src/integrations/prefect-github/prefect_github/credentials.py:56:16-84: Returned type `Literal[b'']` is not assignable to declared return type `str` [bad-return]
- ERROR src/integrations/prefect-github/prefect_github/repository.py:89:16-24: Returned type `Literal[b''] | str` is not assignable to declared return type `str` [bad-return]
- ERROR src/integrations/prefect-gitlab/prefect_gitlab/credentials.py:79:16-84: Returned type `Literal[b'']` is not assignable to declared return type `str` [bad-return]
- ERROR src/integrations/prefect-gitlab/prefect_gitlab/repositories.py:117:16-24: Returned type `Literal[b''] | str` is not assignable to declared return type `str` [bad-return]
- ERROR src/prefect/client/_version_checking.py:67:12-33: Returned type `Literal[b'']` is not assignable to declared return type `str` [bad-return]
- ERROR src/prefect/runner/storage.py:260:16-262:10: Returned type `Literal[b'']` is not assignable to declared return type `str` [bad-return]
- ERROR src/prefect/runner/storage.py:307:61-82: Cannot set item in `dict[str, str]` [unsupported-operation]

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

Primer Diff Classification

❌ 1 regression(s) | ✅ 19 improvement(s) | ❓ 1 needs review | 21 project(s) total | +16, -52 errors

1 regression(s) across aiohttp. error kinds: bad-specialization. caused by as_superclass(). 19 improvement(s) across pip, openlibrary, mkdocs, black, dulwich, yarl, twine, schemathesis, streamlit, zulip, static-frame, mypy, meson, sphinx, PyGithub, spack, poetry, scrapy, prefect.

Project Verdict Changes Error Kinds Root Cause
pip ✅ Improvement -1 unsupported-operation as_superclass()
openlibrary ✅ Improvement -2 bad-return as_superclass()
mkdocs ✅ Improvement -1 bad-return as_superclass()
black ✅ Improvement +1 bad-assignment as_superclass()
cloud-init ❓ Needs Review +4, -8 bad-argument-type, bad-typed-dict-key
dulwich ✅ Improvement -3 bad-argument-type, bad-assignment as_superclass()
yarl ✅ Improvement +1 bad-specialization as_superclass()
twine ✅ Improvement -1 bad-return pyrefly/lib/alt/class/classdef.rs
schemathesis ✅ Improvement -2 bad-return as_superclass()
aiohttp ❌ Regression +10 bad-specialization as_superclass()
streamlit ✅ Improvement -1 bad-argument-type as_superclass()
zulip ✅ Improvement -1 bad-return as_superclass()
static-frame ✅ Improvement -1 bad-return as_superclass()
mypy ✅ Improvement -7 invalid-inheritance is_interface()
meson ✅ Improvement -3 bad-argument-type, bad-assignment as_superclass()
sphinx ✅ Improvement -3 urlunparse/urlunsplit overload resolution fix as_superclass()
PyGithub ✅ Improvement -1 bad-return as_superclass()
spack ✅ Improvement -4 bad-argument-type, bad-return as_superclass()
poetry ✅ Improvement -2 bad-return, missing-attribute as_superclass()
scrapy ✅ Improvement -1 bad-argument-type as_superclass()
prefect ✅ Improvement -10 bad-return, unsupported-operation as_superclass()
Detailed analysis

❌ Regression (1)

aiohttp (+10)

The PR's as_superclass change correctly fixes the urlunparse issue but introduces a side effect: when tuple methods using Self@tuple are invoked on NamedTuple subclasses like aiohttp's WSMessageText, the type checker now emits bad-specialization errors because it fails to recognize that the NamedTuple subclass satisfies the upper bound tuple[_T_co, ...] of the Self type variable. Since NamedTuple subclasses are always subtypes of tuple (they inherit from it), they should satisfy this bound. These errors are false positives — a regression introduced by the PR. The 0/10 mypy/pyright co-report rate confirms this is a pyrefly-specific issue.
Attribution: The change to as_superclass() in pyrefly/lib/alt/class/classdef.rs introduced a new code path that routes NamedTuple subclasses through erase_tuple_type() for tuple-related superclass lookups. This new path appears to break the assignability of NamedTuple subclasses to Self@tuple's upper bound tuple[_T_co, ...], producing spurious bad-specialization errors on aiohttp's WSMessageText and similar NamedTuple types.

✅ Improvement (19)

pip (-1)

This is a clear improvement. The removed error was a false positive where pyrefly incorrectly inferred urllib.parse.urlunsplit() as returning bytes instead of str when called with a SplitResult. The root cause was that NamedTuple subclasses were treated as tuple[Any, ...], making them assignable to Iterable[None], which caused the wrong overload to be selected. The PR fixes this by using the actual element types of NamedTuple subclasses for superclass lookups, so SplitResult correctly matches Iterable[str] and the str-returning overload is chosen. Line 181's result.scheme + ret[4:] is a valid str + str operation.
Attribution: The fix is in pyrefly/lib/alt/class/classdef.rs in the as_superclass() method. The new branch handles NamedTuple subclasses by routing tuple-related superclass lookups through an erased tuple type that uses the actual element types instead of Any. This means SplitResult (a NamedTuple with str fields) is now seen as Iterable[str] rather than Iterable[Any], so it no longer matches the Iterable[None] parameter of the bytes overload of urlunsplit. Additionally, pyrefly/lib/alt/class/class_metadata.rs was changed to allow NamedTuple subclasses with multiple bases in .pyi stub files (needed for typeshed's ParseResult/SplitResult definitions which mix in additional classes).

openlibrary (-2)

Both removed errors were false positives caused by pyrefly's incorrect overload resolution for urlunparse/urlunsplit. The root cause was that NamedTuple subclasses like ParseResult and SplitResult were treated as tuple[Any, ...] for superclass lookups, making them match Iterable[None], which caused the bytes-returning overload to be selected instead of the str-returning one. The PR fixes this by using the actual element types of the NamedTuple when checking assignability to Sequence/Iterable/etc. This is a clear improvement — pyrefly now correctly infers str return types for these URL manipulation functions when given string-based parse results.
Attribution: The fix is in pyrefly/lib/alt/class/classdef.rs in the as_superclass() method. The new code (lines 195-207) detects when a class is a NamedTuple subclass and routes tuple-related superclass lookups through an erased tuple type using the actual element types instead of Any. This prevents ParseResult (which has str fields) from matching Iterable[None], so urlunparse now correctly selects the str overload. Additionally, pyrefly/lib/alt/class/class_metadata.rs was changed to allow NamedTuple subclasses with multiple bases in .pyi stub files (like typeshed's ParseResult which mixes in additional methods), enabling precise type information for these stdlib types.

mkdocs (-1)

This is a clear improvement. The removed error was a false positive caused by incorrect overload resolution. urlunsplit has overloads for str input (returns str) and bytes input (returns bytes). Due to a bug in how pyrefly handled NamedTuple superclass resolution, SplitResult (a NamedTuple with all str fields) was not properly recognized as Iterable[str], which caused the wrong overload to be selected — specifically the bytes overload returning Literal[b'']. The function run_validation on line 511 declares return type str, and urlunsplit(parsed_url) on line 524 does indeed return str at runtime since parsed_url is a SplitResult containing string fields. The PR correctly fixes the NamedTuple superclass resolution so the string overload is chosen, eliminating this false positive.
Attribution: The change to as_superclass() in pyrefly/lib/alt/class/classdef.rs is the primary fix. It adds a new branch that, for NamedTuple subclasses, routes tuple-related superclass lookups through erase_tuple_type() instead of falling through to the tuple[Any, ...] ancestor. This means ParseResult (fields are all str) now appears as tuple[str, ...] for assignability checks against Sequence/Iterable, preventing it from matching Iterable[None]. The companion change in pyrefly/lib/alt/class/class_metadata.rs allows stub files (.pyi) to define NamedTuple subclasses with multiple bases (mixin pattern used by typeshed for ParseResult/SplitResult).

black (+1)

The code {k: (*v,) for k, v in self.file_data.items()} where self.file_data is dict[str, FileData] and FileData is a NamedTuple with fields (float, int, str) is annotated as producing dict[str, tuple[float, int, str]]. The (*v,) syntax unpacks the NamedTuple v and repacks it into a plain tuple. Ideally, the inferred type should be tuple[float, int, str], preserving the fixed-length structure. However, pyrefly infers tuple[float | int | str, ...] instead. This happens because when a NamedTuple is unpacked via the * operator, the type checker treats it as iterating over the NamedTuple. The iteration type of a NamedTuple with fields (float, int, str) is float | int | str (the union of all field types), and repacking with (*v,) produces a variable-length tuple[float | int | str, ...]. This is a known limitation shared by pyright (which also reports this error, as confirmed by the metadata). Mypy does not flag this, suggesting mypy has special handling for unpacking NamedTuples that preserves the fixed-length tuple structure. The code is correct at runtime — unpacking a FileData(float, int, str) and repacking it produces exactly a tuple[float, int, str]. This is a false positive caused by imprecise inference of the (*v,) unpacking pattern for NamedTuples, where the type checker loses the positional type information during the unpack-repack operation.
Attribution: The change to as_superclass() in pyrefly/lib/alt/class/classdef.rs now routes NamedTuple subclasses through erase_tuple_type() for tuple-related superclass lookups. Previously, NamedTuple subclasses used tuple[Any, ...] as their tuple ancestor, so (*v,) where v: FileData would produce tuple[Any, ...] which is assignable to tuple[float, int, str]. Now with the erased tuple type using actual element types, the unpacking (*v,) produces tuple[float | int | str, ...] (a homogeneous variable-length tuple with the union of element types), which is NOT assignable to the fixed-length tuple[float, int, str]. This is a side effect of the NamedTuple tuple-type erasure change.

dulwich (-3)

These three removed errors were all false positives stemming from a single root cause: pyrefly's incorrect handling of NamedTuple subclass assignability. ParseResult is a NamedTuple with str fields, but pyrefly was treating it as tuple[Any, ...], making it assignable to Iterable[None], which caused urlunparse to select the wrong overload (the bytes one returning Literal[b'']). The PR fixes this by using the actual element types of NamedTuple subclasses for superclass lookups, so ParseResult now correctly matches Iterable[str] but not Iterable[None], and urlunparse correctly returns str.
Attribution: The fix has two parts: (1) In pyrefly/lib/alt/class/class_metadata.rs, the NamedTuple multi-base restriction was relaxed for .pyi stub files, allowing typeshed's ParseResult (which mixes NamedTuple with other bases) to be modeled precisely. (2) In pyrefly/lib/alt/class/classdef.rs, the as_superclass() method was changed so that NamedTuple subclasses route tuple-related superclass lookups through an erased tuple class using actual element types instead of Any. This prevents ParseResult from matching Iterable[None], which was why urlunparse incorrectly chose the Literal[b''] overload.

yarl (+1)

This is a legitimate type checking improvement. The PR changed NamedTuple handling so that tuple-related operations use actual element types instead of Any. As a side effect, tuple.__new__(SplitResult, ...) now correctly checks whether SplitResult satisfies the Self type variable's upper bound tuple[_T_co, ...]. Pyright also flags this same issue, providing strong corroboration that this is a real type system concern. The code works at runtime but has a type-level issue with the Self bound on tuple.__new__.
Attribution: The change to as_superclass() in pyrefly/lib/alt/class/classdef.rs now routes NamedTuple subclasses through erase_tuple_type() for tuple-related superclass lookups. This changes how SplitResult is evaluated against the Self type variable bound on tuple.__new__, causing it to now correctly detect the bound violation. The change in pyrefly/lib/alt/class/class_metadata.rs also allows stub-defined NamedTuple mixins (like SplitResult in typeshed) to be processed more precisely.

twine (-1)

This is a clear improvement. The removed error was a false positive caused by pyrefly's incorrect handling of NamedTuple subclass assignability. ParseResult has all str fields, so urlunparse(parsed) returns str, matching the declared return type. The PR fixes the root cause by making NamedTuple subclasses use their actual element types (instead of Any) when checking assignability to Sequence/Iterable/etc., which allows correct overload resolution for urlunparse.
Attribution: The fix is in pyrefly/lib/alt/class/classdef.rs in the as_superclass method. The new branch detects when a class has tuple structure (via as_tuple) and routes superclass lookups through an erased tuple type using actual element types instead of Any. This means ParseResult (a NamedTuple with str fields) is now seen as tuple[str, ...] for assignability purposes, so it matches Iterable[str] (not Iterable[None]), and urlunparse correctly resolves to the str-returning overload. Additionally, pyrefly/lib/alt/class/class_metadata.rs was changed to allow NamedTuple subclasses with multiple bases in .pyi stub files (needed for typeshed's ParseResult which mixes in additional classes).

schemathesis (-2)

These were clear false positives. urlunsplit and urlunparse have overloads for str and bytes inputs. ParseResult and SplitResult are NamedTuples with str fields, so the str-returning overload should be selected. Pyrefly was incorrectly selecting the bytes overload because NamedTuple subclasses were being upcast to tuple[Any, ...], making them assignable to Iterable[None] (which matched the bytes overload's constraints). The PR fixes this by using the actual element types of the NamedTuple for superclass lookups.
Attribution: The fix has two parts: (1) In pyrefly/lib/alt/class/classdef.rs, the as_superclass() method was changed so that NamedTuple subclasses route tuple-related superclass lookups through an erased tuple type using actual element types instead of Any. This means ParseResult (with str fields) is now seen as tuple[str, str, str, str, str, str] for assignability checks, not tuple[Any, ...]. (2) In pyrefly/lib/alt/class/class_metadata.rs, the multiple-inheritance restriction for NamedTuples was relaxed for .pyi stub files, allowing typeshed's ParseResult (which mixes a NamedTuple with additional methods) to be modeled precisely. Together, these changes ensure urlunsplit/urlunparse correctly resolve to the str overload when given ParseResult/SplitResult arguments.

streamlit (-1)

This is a clear improvement. The removed error was a false positive caused by pyrefly incorrectly resolving urlunparse's overload. Looking at the code, parts is created as list(urllib.parse.urlparse(url)) which produces a list[str], then tuple(parts) is passed to urlunparse. The urlunparse function in typeshed has overloads distinguishing between str-based and bytes-based inputs. The old behavior likely failed to properly infer the element type when converting list[str] to a tuple via tuple(), causing it to match the wrong (bytes) overload and infer a return type of Literal[b''] instead of str. The PR fixes the root cause by improving how element types are resolved in such conversions, allowing the correct str overload to be selected. Since urlunparse with string components correctly returns str, which is assignable to HTTPRequest | str, the error was indeed a false positive.
Attribution: The fix is in pyrefly/lib/alt/class/classdef.rs in the as_superclass() method. The new branch (lines added) checks if a class has a tuple representation via as_tuple() and, if so, routes superclass lookups through an erased tuple type with actual element types instead of Any. This means ParseResult (with string fields) now matches Iterable[str] instead of Iterable[Any]/Iterable[None], causing urlunparse to correctly select the str overload. Additionally, pyrefly/lib/alt/class/class_metadata.rs was changed to allow NamedTuple subclasses with multiple bases in .pyi stub files (like typeshed's ParseResult which mixes in additional methods), enabling precise type information for these stdlib types.

zulip (-1)

This is a clear improvement. The removed error Returned type Literal[b''] is not assignable to declared return type str was a false positive caused by incorrect overload resolution. urlunsplit called with a SplitResult (which is a NamedTuple containing string components) should return str, not Literal[b'']. Looking at the code: split_url = urlsplit(url) where url is a str, so split_url is a SplitResult (the str variant). Then modified_url = split_url._replace(netloc=new_hostname) returns another SplitResult. urlunsplit has overloads - one for sequence of str returning str, and one for sequence of bytes returning bytes. The bug was that pyrefly's overload resolution incorrectly matched the bytes overload for SplitResult, likely because of how it handled NamedTuple subclass assignability to the overload parameter types, causing it to infer the return type as bytes (specifically Literal[b'']) instead of str. The PR correctly fixes the inference so that SplitResult properly matches the str overload, and final_url is correctly typed as str.
Attribution: The key change is in as_superclass() in pyrefly/lib/alt/class/classdef.rs, which now routes NamedTuple subclass superclass lookups through erase_tuple_type() instead of falling back to tuple[Any, ...]. This means ParseResult/SplitResult (NamedTuple subclasses with str fields) are now seen as Iterable[str] rather than Iterable[Any], preventing them from matching the Iterable[bytes] overload of urlunparse/urlunsplit. Additionally, the change in class_metadata.rs allows stub files (.pyi) to define NamedTuple subclasses with multiple bases (like ParseResult in typeshed which mixes in additional methods), which was previously rejected.

static-frame (-1)

This is a clear improvement. The removed error Returned type Literal[b''] is not assignable to declared return type str was a false positive caused by pyrefly selecting the wrong overload of urlunparse. urlparse() returns ParseResult, a NamedTuple with all str fields. urlunparse in typeshed has two overloads — one accepting tuple[bytes, bytes, bytes, bytes, bytes, bytes] returning bytes, and one accepting tuple[str, str, str, str, str, str] returning str. Because NamedTuple subclasses were previously represented with an imprecise tuple supertype (e.g., tuple[Any, ...] rather than the specific element types), ParseResult did not match the tuple[str, str, str, str, str, str] overload. The bytes overload was then selected instead (or matched first), resulting in the return type being inferred as bytes (specifically Literal[b'']). The PR correctly fixes this by using the actual element types of the NamedTuple when checking assignability to tuple supertypes, allowing the string overload to be properly selected.
Attribution: The fix is in pyrefly/lib/alt/class/classdef.rs in the as_superclass() method. The new branch (lines 195-207) detects when a class is a tuple-like type (e.g., a NamedTuple subclass) and routes superclass lookups through an erased tuple class that preserves the actual element types. This prevents ParseResult (a NamedTuple with str fields) from being treated as tuple[Any, ...] and incorrectly matching Iterable[None]. Additionally, pyrefly/lib/alt/class/class_metadata.rs was changed to allow NamedTuple subclasses with multiple bases in .pyi stub files (needed for typeshed's ParseResult definition which mixes in additional classes).

mypy (-7)

All 7 removed errors were false positives on typeshed .pyi stub files. Typeshed uses patterns involving NamedTuple with multiple inheritance to model stdlib classes. In urllib/parse.pyi, base classes like _DefragResultBase(NamedTuple, Generic[AnyStr]), _SplitResultBase(NamedTuple, Generic[AnyStr]), and _ParseResultBase(NamedTuple, Generic[AnyStr]) combine NamedTuple with Generic, and then result classes like DefragResult(_DefragResultBase[str], _ResultMixinStr) inherit from a NamedTuple subclass plus a mixin class. In ssl.pyi, Purpose(_ASN1Object, enum.Enum) inherits from both a NamedTuple subclass (_ASN1Object which extends _ASN1ObjectBase(NamedTuple)) and enum.Enum. These are well-established patterns in typeshed accepted by mypy and pyright. Pyrefly was incorrectly applying the runtime restriction against NamedTuple multiple inheritance to stub files where these restrictions don't apply (stubs describe type structure, not runtime behavior). The PR correctly scopes this check to user code only (!cls.module().path().is_interface()), fixing these false positives.
Attribution: The change in pyrefly/lib/alt/class/class_metadata.rs at line 288 adds the condition && !cls.module().path().[is_interface()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/class/class_metadata.rs) to the multiple-inheritance check for NamedTuples. This means the invalid-inheritance error is now only emitted for user code (.py files), not for stub files (.pyi files). This directly removes the 7 false positive errors on typeshed stubs. The additional changes in pyrefly/lib/alt/class/classdef.rs (the as_superclass method) fix the NamedTuple superclass resolution to use erased tuple element types instead of tuple[Any, ...], which is the core fix for issue #2867 but doesn't directly cause the error removals.

meson (-3)

These were clear false positives. The urlunparse function has two overloads in typeshed: one accepting tuple[str, str, str, str, str, str] returning str, and one accepting tuple[bytes, bytes, bytes, bytes, bytes, bytes] returning bytes. ParseResult is a NamedTuple with all str fields, so parts._replace(...) returns a ParseResult which should match the str-based overload. Pyrefly was incorrectly resolving the NamedTuple's superclass to an imprecise tuple type (e.g., tuple[Any, ...]) rather than the specific tuple[str, str, str, str, str, str], which prevented it from matching the str overload and caused it to incorrectly select the bytes overload, producing Literal[b'']. The PR fixes the NamedTuple superclass resolution to use actual element types, allowing ParseResult to correctly match tuple[str, str, str, str, str, str] and thus selecting the correct str overload.
Attribution: The fix has two parts:

  1. In pyrefly/lib/alt/class/classdef.rs, the as_superclass() method was changed to route NamedTuple subclasses through their erased tuple element types instead of tuple[Any, ...] when checking superclass relationships. This means ParseResult (containing str fields) is now seen as tuple[str, str, str, str, str, str] rather than tuple[Any, ...], so it no longer matches Iterable[None].
  2. In pyrefly/lib/alt/class/class_metadata.rs, the multiple-inheritance restriction for NamedTuples was relaxed for .pyi stub files, allowing typeshed's ParseResult definition (which mixes in additional classes) to be modeled precisely.

Together, these changes fix the overload resolution so urlunparse(ParseResult) correctly selects the str overload.

sphinx (-3)

urlunparse/urlunsplit overload resolution fix: All three errors stem from the same root cause: pyrefly's NamedTuple-to-tuple upcast used tuple[Any, ...] instead of the actual element types, causing ParseResult (all str fields) to match the bytes overload of urlunparse/urlunsplit. Removing these false positives is an improvement.

Overall: The analysis is factually correct. urlparse returns a ParseResult (a NamedTuple with all str fields), and urlsplit returns a SplitResult (also a NamedTuple with all str fields). Both urlunparse and urlunsplit have overloaded signatures that dispatch based on whether the input contains str or bytes elements. If pyrefly incorrectly upcasts NamedTuple types to tuple[Any, ...] for superclass resolution, the element types become Any, which could match Iterable[None] or the bytes overload, causing the functions to be inferred as returning bytes (specifically Literal[b'']) instead of str. The _replace method on ParseResult returns another ParseResult (still all str fields), and list(parts) on a SplitResult produces list[str]. In all three cases, the functions should return str, making these errors false positives. The fix to use actual NamedTuple element types for tuple-related superclass resolution correctly addresses the root cause.

Attribution: The change to as_superclass() in pyrefly/lib/alt/class/classdef.rs is the primary fix. It adds a new branch that, for NamedTuple subclasses, routes tuple-related superclass lookups through an erased tuple class using actual element types instead of Any. This means ParseResult (a NamedTuple with str fields) now correctly appears as tuple[str, str, str, str, str, str] for assignability checks against Iterable/Sequence, so urlunparse picks the str overload. The supporting change in pyrefly/lib/alt/class/class_metadata.rs allows stub files (.pyi) to define NamedTuple subclasses with multiple bases (like ParseResult which mixes in additional methods), which was previously rejected.

PyGithub (-1)

This is a clear improvement. The removed error Returned type Literal[b''] is not assignable to declared return type str was a false positive caused by pyrefly incorrectly resolving the overload of urlunparse. Because NamedTuple subclasses were treated as tuple[Any, ...], ParseResult could match Iterable[None], causing pyrefly to select the bytes-returning overload of urlunparse. The PR fixes this by using the actual element types of NamedTuple subclasses for superclass lookups, so ParseResult correctly matches Iterable[str] and the str-returning overload is selected.
Attribution: The fix in pyrefly/lib/alt/class/classdef.rs in as_superclass() adds a new branch that routes NamedTuple subclass superclass lookups through an erased tuple type using actual element types instead of Any. This means ParseResult (a NamedTuple with str fields) is now correctly seen as tuple[str, str, str, str, str, str] for assignability purposes, rather than tuple[Any, ...]. This prevents it from matching Iterable[None], so urlunparse correctly selects the str-returning overload instead of the bytes-returning one. Additionally, pyrefly/lib/alt/class/class_metadata.rs was changed to allow NamedTuple subclasses with multiple bases in .pyi stub files (like typeshed's ParseResult which mixes in additional methods), enabling precise type information to flow through.

spack (-4)

All four removed errors were false positives caused by incorrect overload resolution for urlunparse. The bug was that ParseResult (a NamedTuple with str fields) was treated as tuple[Any, ...] during superclass checks, making it match Iterable[None] and selecting the wrong urlunparse overload (the one returning Literal[b'']). The PR fixes this by routing NamedTuple superclass lookups through erased tuple types that preserve the actual element types. This is a clear improvement — the code is correct and pyrefly was producing wrong type errors.
Attribution: The fix is in pyrefly/lib/alt/class/classdef.rs in the as_superclass() method. A new branch was added: when a class is a NamedTuple subclass (detected via as_tuple), superclass lookups for tuple-related types (like Iterable, Sequence) now go through erase_tuple_type(tuple) which uses the actual element types instead of Any. This means ParseResult (fields are all str) now matches Iterable[str] instead of Iterable[Any]/Iterable[None], so urlunparse correctly resolves to the str-returning overload. Additionally, pyrefly/lib/alt/class/class_metadata.rs was changed to allow NamedTuple multi-inheritance in .pyi stub files (needed for typeshed's ParseResult definition which mixes in additional classes).

poetry (-2)

This is a clear improvement. The PR fixes a bug in pyrefly's handling of NamedTuple subclass superclass resolution. Previously, NamedTuple subclasses like ParseResult were treated as tuple[Any, ...] when checking assignability to Iterable/Sequence, causing ParseResult to match Iterable[None] and triggering the wrong overload of urlunparse (the one returning Literal[b'']). The fix correctly erases the tuple element types so ParseResult is seen as Iterable[str], selecting the correct str-returning overload. Both removed errors were false positives stemming from this single root cause.
Attribution: The change to as_superclass() in pyrefly/lib/alt/class/classdef.rs is the primary fix. It adds a new branch that routes NamedTuple subclasses through their erased tuple element types instead of falling back to tuple[Any, ...]. This means ParseResult (a NamedTuple with str fields) is now correctly seen as Iterable[str] rather than Iterable[Any], which prevents it from matching the Iterable[None] parameter of the bytes-returning overload of urlunparse. The change to class_metadata.rs allows stub files (.pyi) to define NamedTuple subclasses with multiple bases (like ParseResult in typeshed which mixes in additional methods), which is necessary for the fix to work with stdlib stubs.

scrapy (-1)

This is a clear improvement. The removed error was a false positive caused by pyrefly incorrectly resolving urlunsplit()'s overload. The root cause was that NamedTuple subclasses like SplitResult were treated as tuple[Any, ...], making them assignable to Iterable[None], which caused the bytes overload to be selected. The PR fixes the NamedTuple superclass resolution to use actual element types, so SplitResult (with str fields) correctly matches Iterable[str] and the str overload is selected, returning str instead of Literal[b''].
Attribution: The change to as_superclass() in pyrefly/lib/alt/class/classdef.rs is the primary fix. It adds a new branch that routes NamedTuple subclass superclass lookups through erase_tuple_type() instead of falling through to the NamedTupleFallback path that used tuple[Any, ...]. This means SplitResult (with str fields) now correctly matches Iterable[str] instead of Iterable[Any], so urlunsplit's overload resolution picks the str overload instead of the bytes overload. The additional change in class_metadata.rs allows stub files (.pyi) to define NamedTuple subclasses with multiple bases (mixin pattern used by typeshed for ParseResult/SplitResult).

prefect (-10)

This is a clear improvement. The PR fixes a type inference bug where NamedTuple subclasses (specifically urllib.parse.ParseResult) were being upcast to tuple[Any, ...] instead of preserving their actual element types. This caused urlunparse(ParseResult) to match the wrong overload (the bytes variant), producing Literal[b''] | str instead of str. All 10 removed errors were false positives caused by this incorrect overload resolution. The fix correctly routes NamedTuple superclass lookups through erased tuple types that preserve element type information.
Attribution: The fix is in pyrefly/lib/alt/class/classdef.rs in the as_superclass() method. The new code (lines 195-207) detects when a class is a NamedTuple subclass (via as_tuple) and routes tuple-related superclass lookups through an erased tuple type that preserves the actual element types. This prevents ParseResult (which has fields like scheme: str, netloc: str, etc.) from matching Iterable[None], so urlunparse correctly selects the str overload. Additionally, pyrefly/lib/alt/class/class_metadata.rs was changed to allow NamedTuple subclasses with multiple bases in .pyi stub files (like typeshed's ParseResult which mixes in _ResultMixinStr), while still rejecting this pattern in user code.

❓ Needs Review (1)

cloud-init (+4, -8)

LLM classification failed: Anthropic API returned 429: {"type":"error","error":{"type":"rate_limit_error","message":"This request would exceed the rate limit you configured in workspace Githib_Actions_POC of 10,000 output tokens per minute. For details, refer to: https://docs.claude.com/en/api/rate-limits; see the response headers for current usage. Please reduce the prompt length or the maximum tokens requested, or try again later. You may also adjust your configured workspace rate limit in the settings page of the Anthropic Console."},"request_id":"req_011CZopWp9Rifq8ZHiBpEBBT"}. Non-trivial change (4 added, 8 removed).

Suggested fixes

Summary: The new as_superclass() branch for NamedTuple subclasses intercepts tuple-related superclass lookups too broadly, causing bad-specialization errors when Self@tuple bounds are checked against erased tuple types for NamedTuple subclasses in aiohttp.

1. In as_superclass() in pyrefly/lib/alt/class/classdef.rs, add a guard so the erased-tuple path is NOT taken when want is tuple itself. The erased tuple path should only be used for superclasses above tuple (Sequence, Iterable, Reversible, etc.), not for tuple itself. When want is the tuple class, fall through to the normal ancestor resolution path. Change the condition from !class.[class_object()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/class/classdef.rs).is_builtin("tuple") to also include && !want.is_builtin("tuple"), so that when we're looking for tuple as a superclass of a NamedTuple subclass, we use the normal nominal path instead of the erased tuple path. This preserves the fix for Sequence/Iterable assignability (urlunparse overload resolution) while avoiding the Self@tuple bound-checking regression.

Files: pyrefly/lib/alt/class/classdef.rs
Confidence: high
Affected projects: aiohttp
Fixes: bad-specialization
The 10 bad-specialization errors in aiohttp occur when tuple methods using Self@tuple are called on WSMessageText (a NamedTuple). The Self type variable has upper bound tuple[_T_co, ...]. When as_superclass is called with want=tuple, the erased tuple type (e.g., tuple[str | int | None, ...]) is used, but this erased type may fail the Self bound check differently than the original NamedTuple class would. By excluding want=tuple from the erased path, Self@tuple resolution goes through the normal ancestor chain, which correctly recognizes NamedTuple subclasses as subtypes of tuple. The Sequence/Iterable lookups (want != tuple) still use the erased path, preserving the 19 improvements for urlunparse/urlunsplit overload resolution. This eliminates 10 bad-specialization errors in aiohttp while keeping all improvements.


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (1 heuristic, 20 LLM)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

urlunparse returns Literal[b''] instead of str

2 participants