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:

  1. Configuration.__init__ — reading pip config files with locale-aware encoding
  2. _decode_req_file — decoding requirement file contents when the default encoding fails
  3. Test mocks — updating the test suite to patch both getpreferredencoding and getencoding

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:

  1. Identify all call sites — a single deprecation can appear in multiple functions within the same module
  2. Mock both the old and new APIs in tests — otherwise CI passes on 3.12 but breaks on 3.9
  3. Verify the fallback path — locale.getencoding() can return None on 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.