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.
Table of contents
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.
| Change | Why you'd care | Kind |
|---|---|---|
| Lazy imports (PEP 810) | Faster startup (~4x in my test) | New capability |
| UTF-8 by default (PEP 686) | Less mojibake | Better (mind the migration) |
| New profiler (PEP 799) | Profile a live process without restarting it | New capability |
| Unpacking in comprehensions (PEP 798) | Flatten lists without a nested for | Better |
| locale.getdefaultlocale() | Deprecation reversed β no migration needed | Nothing 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 warningThe 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 situation | What 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.
| Date | Milestone | What it means |
|---|---|---|
| May 7, 2026 | Beta 1 (feature freeze) | Feature set locked |
| Aug 4, 2026 | Release candidate 1 | Bug fixes only (we are here) |
| Sep 1, 2026 | Release candidate 2 | Final check |
| Oct 1, 2026 | Final release | Safe for production |
| ~Oct 2031 | End of support | 2 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.
| Style | Startup (median) | Modules loaded |
|---|---|---|
| Normal imports | 21.1 ms | 122 |
| Lazy imports | 5.2 ms | 26 |
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:
| Build | Normal imports | Lazy imports | Modules loaded |
|---|---|---|---|
| 3.15.0b2 (June) | 21.1 ms | 5.2 ms | 122 β 26 |
| 3.15.0rc1 (August) | 22.6 ms | 5.3 ms | 124 β 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 importworks only at the top level of a file. Not inside functions, not insidetry, and not withfrom 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 cpuI 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) # => MISSINGfrozendict 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.
| Feature | 3.12 | 3.15 | Use instead |
|---|---|---|---|
| sre_compile / sre_parse / sre_constants | present | removed | the re module |
| pathlib.PurePath.is_reserved() | present | removed | os.path.isreserved() |
| http.server.CGIHTTPRequestHandler | present | removed | a real WSGI/ASGI server |
| types.CodeType.co_lnotab | present | removed | co_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?
uv python install 3.15.0rc1.What is the headline feature of Python 3.15?
What changes when UTF-8 becomes the default?
Will any code stop working in Python 3.15?
Do I need to replace locale.getdefaultlocale() in Python 3.15?
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.
- β’ Verification code (GitHub repo) β every snippet and result from this article
- β’ What's New In Python 3.15 (official docs)
- β’ PEP 790 β Python 3.15 Release Schedule
- β’ PEP 810 β Explicit lazy imports
- β’ PEP 686 β Make UTF-8 mode default / PEP 799 β Sampling profiler / PEP 798 β Unpacking in comprehensions
- β’ Deprecations (the removal list)

Backend Engineer / AWS / Django