Top/Articles/Python 3.15: locale.getdefaultlocale Won't Be Removed, Plus Lazy Imports and What Breaks
python-315-changes-cover-en-update

Python 3.15: locale.getdefaultlocale Won't Be Removed, Plus Lazy Imports and What Breaks

Hit the "'locale.getdefaultlocale' is deprecated and slated for removal in Python 3.15" warning? Swap it for getlocale()/getencoding() β€” the replacement code is inside. Plus the rest of 3.15, benchmarked on the beta: lazy imports (~4x faster startup), UTF-8 by default, and the APIs that stop working when you upgrade.

LabPublished June 17, 2026Last updated Aug. 23, 2026
Table of contents
Key takeaways

Hit the "'locale.getdefaultlocale' is deprecated and slated for removal in Python 3.15" warning? Swap it for getlocale()/getencoding() β€” the replacement code is inside. Plus the rest of 3.15, benchmarked on the beta: lazy imports (~4x faster startup), UTF-8 by default, and the APIs that stop working when you upgrade.

The short version: lazy imports finally make startup fast

Python 3.15 is scheduled for October 1, 2026. As always there's a long changelog, but for most developers the one to care about is explicit lazy imports. "Python startup is slow" has been a running complaint for a decade, and 3.15 does something about it: modules aren't loaded until you actually use them, which makes CLI tools noticeably snappier.

For this article I installed the still-beta Python 3.15 (3.15.0b2) and ran each major change by hand. The goal isn't just "here's what's new" β€” it's "here's what used to be annoying, how it gets better, and which old code stops working when you upgrade," with real numbers from a real machine.

Here's the summary up front.

ChangeWhy you'd careKind
Lazy imports (PEP 810)Faster startup (~4x in my test)New capability
UTF-8 by default (PEP 686)Less mojibakeBetter (mind the migration)
New profiler (PEP 799)Profile a live process without restarting itNew capability
Unpacking in comprehensions (PEP 798)Flatten lists without a nested forBetter
locale.getdefaultlocale()Deprecation reversed β†’ no migration neededNothing to do
Removed APIs(migration gotchas)Stops working

All the verification code lives in a public GitHub repo. Run uv python install 3.15 and you can reproduce every number here. The JIT compiler also made a comeback in 3.15, but that's a separate story; this article focuses on everything else.

locale.getdefaultlocale is not being removed after all

If you landed here from a warning like 'locale.getdefaultlocale' is deprecated and slated for removal in Python 3.15, here is the short answer: you do not need to do anything. The removal was called off, and the deprecation itself has been reversed.

This is a correction to an earlier version of this article, which told you to migrate. That advice was based on the state of the beta. It is no longer right, and the reason is worth showing rather than just asserting.

On Python 3.12, the warning is unambiguous and names 3.15 directly:

# Python 3.12.14
DeprecationWarning: 'locale.getdefaultlocale' is deprecated and
slated for removal in Python 3.15.
Use setlocale(), getencoding() and getlocale() instead.

On the 3.15 release candidate, the warning is simply gone β€” and so is the deprecation behind it:

# Python 3.15.0rc1
>>> import warnings, locale
>>> warnings.simplefilter("always")
>>> locale.getdefaultlocale()
('C', 'UTF-8')      # no warning

The change is in the official notes for 3.15: "Undeprecate the locale.getdefaultlocale() function" (contributed by Victor Stinner in gh-130796). Note the word: undeprecate, not "postpone". The function is not on a later removal list. It is a supported function again.

What this means in practice

Your situationWhat to do
You saw the warning and
have not changed anything yet
Nothing. The warning
disappears when you reach 3.15
You already migrated to
getlocale() / getencoding()
Keep it. Those are fine
and arguably clearer
You have a scripted sweep of
deprecation warnings queued up
Drop this one from the list
before it creates noise
The warning is failing your CI
on 3.12 or 3.13 today
Filter that one warning
rather than rewriting the call

If the warning is breaking a build on an older interpreter and you would rather not touch the code, silence just this one:

import warnings
warnings.filterwarnings(
    "ignore",
    message=r".*getdefaultlocale.*",
    category=DeprecationWarning,
)

There is a wider lesson here that is worth more than this one function. A deprecation warning is a proposal, not a schedule. It tells you what the maintainers intended at the time it was written, and intentions get revisited. Warnings that name a specific version are the ones most likely to be quoted back as fact long after the plan changed.

That said, plenty of APIs genuinely did disappear in 3.15, and those will raise real errors on upgrade. I ran them on the release candidate rather than trusting the docs β€” the results are in the migration section further down.

When does Python 3.15 ship, and what's the status now?

The final release is set for October 1, 2026 (per PEP 790, the release schedule). Python ships a new version every October, and 3.15 follows that cadence.

As of August 2026 it has moved past beta into the release candidate phase. The first RC, 3.15.0rc1, landed on August 4, 2026. The part worth internalizing: the feature window closed on May 7 with beta 1. What is in 3.15 is settled β€” nothing gets added or dropped from here. RCs carry bug fixes only.

DateMilestoneWhat it means
May 7, 2026Beta 1 (feature freeze)Feature set locked
Aug 4, 2026Release candidate 1Bug fixes only
(we are here)
Sep 1, 2026Release candidate 2Final check
Oct 1, 2026Final releaseSafe for production
~Oct 2031End of support2 years of bug fixes
+ 3 of security fixes
# Install the release candidate with uv
$ uv python install 3.15.0rc1
$ python3.15 -VV
Python 3.15.0rc1 (main, Aug 14 2026, 15:34:29) [Clang 22.1.3 ]

The original measurements in this article were taken on beta 2 in June. I re-ran the important ones on rc1 for this update, and the results appear alongside the originals below. The performance picture did not move. One item did change, and it is the one people arrive here searching for: a function that was slated for removal is staying.

Lazy imports make startup fast (PEP 810)

The headline feature. Python convention is to put every import at the top of the file β€” which means modules you might never use get loaded at startup anyway. A CLI tool that only wants to print --help still drags in dozens of modules first.

The old workaround was to bury heavy imports inside functions. PEP 810 notes that roughly 17% of stdlib imports are already scattered into functions for exactly this reason. Python 3.15 turns the workaround into a language feature: put lazy in front of the import.

# With `lazy`, json is NOT loaded at this point
lazy import json

print("json in sys.modules before first use:", "json" in sys.modules)
# => False (not loaded yet)

# The first time you actually use the name, the real module loads
json.dumps({"hello": "world"})
print("json in sys.modules after first use: ", "json" in sys.modules)
# => True (loaded here)

Running it confirms the module stays unloaded until first access. The lazy import line binds a lightweight proxy; the first attribute access "reifies" it, swapping in the real module.

How much faster, measured

Numbers, not vibes. I wrote a script that imports five heavyweight stdlib modules (json, pathlib, argparse, logging, http.client) and timed start-to-exit 30 times, once with normal imports and once with lazy imports it never uses.

StyleStartup (median)Modules loaded
Normal imports21.1 ms122
Lazy imports5.2 ms26

Startup dropped from ~21 ms to ~5 ms, roughly 4x, and the module count fell from 122 to 26. This is the best case β€” I imported things and never used them β€” so your mileage depends on what you import. But deferring heavy libraries you don't always need is a real win: a tool that used to stall for a beat on every invocation now feels instant.

I ran the identical script again on 3.15.0rc1 in August:

BuildNormal importsLazy importsModules loaded
3.15.0b2 (June)21.1 ms5.2 ms122 β†’ 26
3.15.0rc1 (August)22.6 ms5.3 ms124 β†’ 32

Two months on, the gap is unchanged. Since the feature set froze in May, this is the behavior that ships in October. The slight rise in the eager module count reflects small stdlib churn, not a regression in the feature.

You can retire those in-function imports

The speed is nice, but the thing I appreciate most is readability. For years the standard trick to keep startup fast was to bury a heavy import inside a function. As mentioned above, even the standard library has roughly 17% of its imports scattered into functions for exactly this reason β€” often not by choice, but to work around the cost of top-level imports.

In 3.15 you can move those back to a lazy import at the top of the file. Your dependencies line up at the top again, so "what does this file depend on?" is answerable at a glance, and you avoid the per-call name resolution that in-function imports pay every time they run. You get the fast startup and the readable dependency list, without choosing between them.

That said, not every in-function import becomes unnecessary. When there's an actual logic reason β€” pulling in a heavy library only on a specific OS or feature path, or deliberately deferring an import to break a circular dependency β€” keeping it inside the function still expresses the intent better (lazy import only works at the top level, and it doesn't magically resolve circular imports). Move the imports you scattered purely for startup speed back to the top, and leave the ones that are there for a reason. Hold that line and your readability genuinely improves.

The gotchas

It's not free. Things I hit while testing:

  • β€’ Module level only. lazy import works only at the top level of a file. Not inside functions, not inside try, and not with from x import *.
  • β€’ Errors move later. A lazy import of a missing module doesn't fail on the import line β€” it fails the first time you use the name.
  • β€’ Import-time side effects don't fire. Libraries that register things on import won't run that code until first use.

An explicit lazy import is honored by default, no flag needed. To flip the whole program's behavior, use the startup option -X lazy_imports=all. One thing that tripped me up in testing: this option is not a bare flag β€” it requires a value of all, none, or normal. Writing just -X lazy_imports errors out at startup.

UTF-8 is finally the default (PEP 686)

In 3.15, the default text encoding becomes UTF-8. If you've ever fought Windows over cp1252 or Japanese cp932, this is the change you've been waiting for.

Until now, open() without an explicit encoding followed the OS locale. On Windows that often meant a legacy code page rather than UTF-8, so reading a UTF-8 file produced mojibake. From 3.15, the default is UTF-8 regardless of locale.

# Run on 3.15 (no encoding specified)
import sys
print("sys.flags.utf8_mode:", sys.flags.utf8_mode)   # => 1

with open("sample.txt", "w") as f:   # no encoding= argument
    print("default open() encoding:", f.encoding)     # => utf-8
    f.write("ζ—₯本θͺžγƒ†γ‚­γ‚Ήγƒˆ")

On 3.15, sys.flags.utf8_mode is 1 (on) with no configuration. On 3.12 the same check returns 0 (off).

To be honest: on Linux and macOS, where the locale is already UTF-8, you'll barely notice β€” open() was opening UTF-8 anyway. The real beneficiary is Windows. The classic "forgot the encoding, got mojibake" bug is much less likely by default.

The flip side: code that relied on a legacy code page may behave differently in 3.15. To restore the old locale-based behavior, set PYTHONUTF8=0 or pass -X utf8=0 at startup, or opt out per file with open(..., encoding="locale").

A built-in profiler you can attach to a live process (PEP 799)

A profiler tells you where your program spends its time. Python 3.15 ships a new one in the standard library, profiling.sampling. It samples "what's running right now" at a fixed rate, so it measures with very little overhead.

The big difference from the old cProfile is that you can attach to an already-running process β€” no code changes, no restart. That "why is this production process pegged?" moment now has a first-party answer.

# Run a script and emit an interactive flamegraph
$ python3.15 -m profiling.sampling run -r 10khz \
    --flamegraph -o flamegraph.html workload.py

# Attach to a running process (PID 12345), no restart needed
$ python3.15 -m profiling.sampling attach 12345 --mode cpu

I profiled a recursive-Fibonacci workload at 10,000 samples/sec. The result made it obvious at a glance that 83.7% of the time went into the recursive Fibonacci call. The flamegraph below (width = share of time spent) is the actual output, embedded as-is. Click a bar to zoom in.

↑ Flamegraph generated by Python 3.15's profiling.sampling (open in a new tab)

Other output formats include a classic cProfile-style table (--pstats), a per-line heatmap (--heatmap), and a real-time terminal view (--live). You can also restrict what you measure to CPU time only or GIL-holding time only. The old cProfile family has been reorganized under the name profiling.tracing.

Small wins: comprehension unpacking, new built-ins, friendlier errors

Unpacking inside comprehensions (PEP 798)

Flattening a list of lists used to need two for clauses. In 3.15 you can unpack with * directly.

lists = [[1, 2], [3, 4], [5]]

# Before (two fors)
old = [x for sub in lists for x in sub]

# 3.15 (unpack with *)
new = [*sub for sub in lists]

print(new)   # => [1, 2, 3, 4, 5]

Dict comprehensions get {**d for d in dicts} too. Not life-changing, but that unreadable nested-for finally has a cleaner form.

An immutable dict and a real sentinel, built in

An immutable mapping, frozendict (PEP 814), and a unique marker value, sentinel (PEP 661), are now built in β€” no third-party package required.

# Immutable dict
config = frozendict(host="localhost", port=8080)
config["port"] = 9090
# => TypeError: 'frozendict' object does not support item assignment

# A marker that's distinct from None
MISSING = sentinel("MISSING")
print(MISSING)   # => MISSING

frozendict is hashable and unchangeable, so you can use it as a dict key or pass it as a config that nobody can mutate. sentinel is the clean way to tell "argument omitted" apart from "None was passed explicitly" β€” the pattern everyone used to hand-roll is now standard.

Error messages that help refugees from other languages

If you reach for a method by its name in another language, Python now suggests the right one. Actual 3.15 output:

>>> [1, 2, 3].push(4)
AttributeError: 'list' object has no attribute 'push'. Did you mean '.append'?

>>> "hello".toUpperCase()
AttributeError: 'str' object has no attribute 'toUpperCase'. Did you mean '.upper'?

>>> {}.put("a", 1)
AttributeError: 'dict' object has no attribute 'put'. Use d[k] = v.

JavaScript's .push() and Java's .put() get gently redirected to the Pythonic spelling. One note from testing: that suggestion is added when the exception is rendered, so catching it and printing str(exc) won't show it β€” format it via the traceback module and it appears.

Code that stops working in 3.15 (migration gotchas)

Jump straight to 3.15 from an old version and some code that used to work will break. A few long-deprecated features are finally gone. I ran the same script on 3.12 and 3.15 to see exactly what disappears.

Feature3.123.15Use instead
sre_compile / sre_parse / sre_constantspresentremovedthe re module
pathlib.PurePath.is_reserved()presentremovedos.path.isreserved()
http.server.CGIHTTPRequestHandlerpresentremoveda real WSGI/ASGI server
types.CodeType.co_lnotabpresentremovedco_lines()
locale.getdefaultlocale()present (warns)stays (undeprecated)nothing β€” keep using it

Here's the actual run. [GONE] means removed; [OK] means still usable.

# Re-run on Python 3.15.0rc1 (August 2026)
  [GONE] sre_compile module: ModuleNotFoundError
  [GONE] sre_parse module: ModuleNotFoundError
  [GONE] sre_constants module: ModuleNotFoundError
  [GONE] pathlib.PurePath.is_reserved(): AttributeError
  [GONE] http.server.CGIHTTPRequestHandler: AttributeError
  [GONE] types.CodeType.co_lnotab: AttributeError
  [OK]   locale.getdefaultlocale()

sre_compile and friends are internal regex-engine modules you'd rarely import directly. But some libraries poke at them internally, so if a dependency suddenly breaks on 3.15, this is a likely culprit.

The six above behave identically on the release candidate and on beta 2, so treat them as settled for the final release.

The last row is the one that changed. On 3.12, locale.getdefaultlocale() explicitly warns that it is "slated for removal in Python 3.15" β€” but on 3.15.0rc1 it runs with no warning at all, because the deprecation was reversed rather than merely postponed. That is the one item on this page you can cross off your migration list entirely; the details are in the section near the top of this article.

Separately, re.match() β€” long confusing because it matches at the start of the string rather than anywhere β€” gets a clearer alias, re.prefixmatch(), and re.match() is now soft-deprecated (not going away immediately).

Should you upgrade to 3.15?

The final release is October 1, 2026 β€” about five weeks out as this update goes live β€” so this isn't a "bump production today" situation. But 3.15 looks worth the wait. Lazy imports in particular pay off clearly for CLI tools that suffered from slow startup and for apps that pull in a lot of libraries.

The flip side is that UTF-8-by-default and the removed APIs can change behavior during migration. The fix is simple: install the release candidate and run your code and tests through it once. Because the feature set was frozen back on May 7, anything that passes on the RC will pass on the final release. uv python install 3.15.0rc1 installs it side by side, so leave your current Python alone and try it in CI. The verification code for this article is in the GitHub repo β€” a good starting point for your own checks.

When is Python 3.15 released?
The final release is scheduled for October 1, 2026. The first release candidate (3.15.0rc1) shipped on August 4, 2026 and a second is due September 1. The feature set was frozen on May 7, so what is in the RC is what ships. Try it with uv python install 3.15.0rc1.
What is the headline feature of Python 3.15?
Explicit lazy imports. Modules load only on first use. In a stdlib-heavy startup that never used the imports, startup got about 4x faster and the module count dropped from 122 to 26.
What changes when UTF-8 becomes the default?
Files opened without an explicit encoding are treated as UTF-8, which mainly fixes mojibake on Windows (where the default was a legacy code page). Restore the old behavior with PYTHONUTF8=0 or -X utf8=0.
Will any code stop working in Python 3.15?
Yes. sre_compile and related internal modules, pathlib's is_reserved(), http.server's CGIHTTPRequestHandler, and types.CodeType.co_lnotab are removed. I confirmed this by running the same script on 3.12 and 3.15.0rc1. locale.getdefaultlocale() is the exception: despite the warning naming 3.15, its deprecation was reversed and it still works with no warning.
Do I need to replace locale.getdefaultlocale() in Python 3.15?
No. The deprecation was reversed in 3.15 (CPython gh-130796), so the function is supported again and the warning disappears once you upgrade. On 3.15.0rc1 it returns a value with no warning at all. If you already switched to locale.getlocale() and locale.getencoding(), keep that β€” it still works. If the warning is failing CI on 3.12 or 3.13, filter that single warning instead of rewriting the call.

Test environment and sources

Environment: the original measurements used Python 3.15.0b2 (built June 11, 2026) / Linux aarch64 against Python 3.12.13. The August 2026 update re-ran the key checks on Python 3.15.0rc1 (built August 14, 2026) / Linux aarch64 against Python 3.12.14 β€” lazy-import startup, the removed APIs, the default UTF-8 mode, and the newly added types and functions. All numbers are measured locally. The feature set was frozen on May 7, 2026, so behavior seen on the release candidate is what ships on October 1.

avatar-m-1

Backend Engineer / AWS / Django