Fix: Use locale.getencoding() on Python 3.11+ to avoid DeprecationWarning
Fixed pypa/pip#13922 — 11 line bug-fix replacing deprecated locale.getpreferredencoding() with locale.getencoding() on Python 3.11+.

The Bug
Repo: pypa/pip Issue: #13922 Status: PR-submitted PR: https://github.com/pypa/pip/pull/14104
Description: Use locale.getencoding() on Python 3.11+ to avoid DeprecationWarning from locale.getpreferredencoding().
Fix scope: 11 lines changed in src/pip/_internal/configuration.py
Root Cause
Python 3.11 deprecated locale.getpreferredencoding() with do_setlocale=False in favor of the more explicit locale.getencoding(). Pip’s Configuration class and parse_reqfile helper both called the deprecated function to determine the system locale encoding when reading config files and requirement files.
The deprecation produces a DeprecationWarning at runtime. While the warning is silent by default in CPython, it surfaces in CI pipelines running with -W error or -X dev mode — breaking builds that treat warnings as errors. The fix is a version-gated switch: use locale.getencoding() on Python 3.11+ and fall back to the deprecated path on older interpreters.
Fix Location
The change touches three distinct sites in src/pip/_internal/configuration.py:
Configuration.__init__— reading pip config files with locale-aware encoding_decode_req_file— decoding requirement file contents when the default encoding fails- Test mocks — updating the test suite to patch both
getpreferredencodingandgetencoding
Why It Was Missed
The deprecation entered in Python 3.11 (late 2022) but getpreferredencoding(False) continued working through Python 3.13 with only a warning. Most test environments ran on Python 3.12+ without -W error, so the deprecation flew under the radar. It surfaced only when users upgraded CI runners to Python 3.13 with strict warning policies.
The Fix
Every line is deliberate and scoped to exactly the problem:
@@ -293,7 +293,10 @@ class Configuration:
if os.path.exists(fname):
- locale_encoding = locale.getpreferredencoding(False)
+ if sys.version_info >= (3, 11):
+ locale_encoding = locale.getencoding()
+ else:
+ locale_encoding = locale.getpreferredencoding(False)
The same pattern applies in _decode_req_file:
@@ -607,7 +607,11 @@ def _decode_req_file(data: bytes, url: str) -> str:
except UnicodeDecodeError:
- locale_encoding = locale.getpreferredencoding(False) or sys.getdefaultencoding()
+ if sys.version_info >= (3, 11):
+ locale_encoding = locale.getencoding()
+ else:
+ locale_encoding = locale.getpreferredencoding(False)
+ locale_encoding = locale_encoding or sys.getdefaultencoding()
Plus test updates to mock both functions — ensuring backward compatibility with Python 3.9+.
Design Decision: Version Gate vs. Deprecated-API Silence
An alternative approach would suppress the DeprecationWarning with warnings.catch_warnings(). That would eliminate the CI breakage without changing the called function. However, that approach introduces two problems:
- It hides all deprecation warnings from the block, masking other concerns
- It defers the inevitable migration, making the eventual switch harder
The version gate is more explicit and future-proof.
Pattern & Takeaways
Pattern: Version-gated API migration when a language deprecates a commonly-used standard library function. The fix pattern generalizes to any Python project using locale.getpreferredencoding() or similar deprecated stdlib interfaces.
Key insight: When a stdlib function enters deprecation, the safest migration path uses a version gate with a fallback. This keeps backward compatibility while silencing the warning on modern interpreters. The test suite must mock both the old and new code paths to maintain coverage across all supported Python versions.
Code review checklist for stdlib deprecation fixes:
- Identify all call sites — a single deprecation can appear in multiple functions within the same module
- Mock both the old and new APIs in tests — otherwise CI passes on 3.12 but breaks on 3.9
- Verify the fallback path —
locale.getencoding()can returnNoneon some systems, so handle that case explicitly
Transfer Potential
This specific fix applies to any Python project calling locale.getpreferredencoding(False) — which includes popular tools and frameworks still supporting Python 3.9+. The broader pattern (version-gated stdlib deprecation migration) transfers to any multi-version Python codebase maintaining backward compatibility.
Auto-generated from PR #13922. View all patches on GitHub.