Enumeration disagreed with other filesystem APIs.
CPython issue #150880 tracked inconsistent Windows behavior for paths whose final component ended with a space. APIs such as open(), os.stat(), and os.path.exists() resolved the path after normal Windows normalization, while os.listdir() and os.scandir() could fail to enumerate the same directory.
PR #152906, authored by Zain Nadeem, changed how the Windows enumeration search path is constructed. It normalizes ordinary Windows paths before adding the wildcard consumed by FindFirstFileW(), while keeping \\?\ extended paths literal.
The wildcard moved the trailing space into the middle of the path.
Windows enumeration does not pass the directory path alone to FindFirstFileW(). CPython appends a wildcard such as *.*. A path ending in a space therefore changes shape before the operating-system call: the space is no longer at the end of the full search string; it becomes part of an internal component followed by a separator and wildcard.
directory-with-space␠directory-with-space␠\*.*That distinction affected Windows trimming behavior and made enumeration disagree with APIs operating on the base path directly.
Normalization happened too late—or not at all—for the search path.
The former list-directory and scandir paths allocated a buffer, copied the supplied base path, and appended the wildcard. Because the base path had not first been resolved through Windows full-path normalization, the final search string preserved a component shape that Windows interpreted differently.
The solution could not normalize every path unconditionally. Extended paths beginning with \\?\ intentionally bypass normal Win32 path processing and must retain their literal semantics.
Normalize only the wildcard path used for enumeration.
The merged patch extends CPython’s Windows path-joining helper with a normalization flag. When building the wildcard search path for listdir() or scandir(), the helper calls CPython’s full-path routine for non-extended paths before joining *.*. When building each returned DirEntry.path, it leaves the supplied path representation unchanged.
// Simplified policy
if (normalize && !is_extended_path(path)) {
path = get_full_path_name(path);
}
return join(path, filename);This separates two responsibilities: normalize the internal query sent to Windows, but preserve the caller-facing path construction that existing code expects.
Compare enumeration with direct path operations.
The Windows-only regression creates a directory and then addresses it through a path containing a trailing space. It confirms that exists(), stat(), file opening, listdir(), scandir(), and pathlib.Path.iterdir() agree. It separately verifies that the corresponding extended path is not normalized and continues to fail as a literal non-existent path.
| Scenario | Protected behavior |
|---|---|
| Ordinary trailing-space path | Enumeration matches other normalized filesystem APIs |
DirEntry construction | Caller-facing path representation remains stable |
Extended \\?\ path | Literal extended-path semantics are preserved |
The targeted Windows behavior was merged with regression coverage.
Zain authored PR #152906. The change touched Modules/posixmodule.c, the Windows scandir tests, and a library news entry. It was merged on 3 July 2026 as commit 1b4135a2c69f5320dbdcb37dcc295a5fd36580b5.
Internal query construction can change platform semantics.
- Test filesystem APIs as a family when they transform the same user path differently.
- Normalize at the boundary where platform interpretation matters, not indiscriminately throughout the public API.
- Extended path prefixes are explicit contracts; compatibility fixes should preserve their literal behavior.
- Regression tests should include both the corrected ordinary path and the intentionally unchanged special case.