Porting to Python 3.15: What Breaks and How to Fix It
What actually breaks when you upgrade to Python 3.15: UTF-8 default encoding, removed APIs, sqlite3 and argparse changes, and what’s deprecated for 3.16
Most code ports to Python 3.15 without changes — the real work is a short list of removed APIs, a handful of signature changes, and one behaviour shift: UTF-8 is now the default encoding. Python 3.15.0 was released on 9 October 2026. The official porting section is short, and this guide turns it into a checklist you can run against your own code. For what’s new in the release rather than what breaks, see Python 3.15: What’s New.
Find problems before they find you
Run your test suite on 3.15 with deprecation warnings escalated to errors:
python3.15 -W error::DeprecationWarning -m pytest
Anything that was deprecated in 3.13 or 3.14 and removed in 3.15 will now fail loudly in CI instead of silently in production. Add 3.15 to your CI matrix as an allowed-to-fail job first, then promote it once it’s green. pytest is where most of these failures will surface, and Ruff’s upgrade rules can modernise some of the older patterns for you.
The change most likely to affect you: UTF-8 by default
Python now uses UTF-8 as its default encoding, independent of the system’s environment. I/O without an explicit encoding, like open("notes.txt"), uses UTF-8 (PEP 686).
This only matters if you relied on the previous platform default, which most often means files written by other tools in a legacy encoding on Windows. The fix is the same one that has always been recommended:
# Explicit and portable across every Python version
with open("notes.txt", encoding="utf-8") as f:
...
# If you genuinely want the system's encoding
with open("legacy.txt", encoding="locale") as f:
...
encoding="locale" has been supported since Python 3.10. To find calls that would behave differently, use Python’s opt-in encoding warning. As a temporary escape hatch, UTF-8 mode can be disabled with PYTHONUTF8=0 or -X utf8=0, but treat that as a bridge, not a destination.
Behaviour changes from the porting section
These are the items the official notes list as potentially requiring code changes:
sqlite3: all parameters ofsqlite3.connect()exceptdatabaseare now keyword-only. The first three parameters ofcreate_function()andcreate_aggregate()are positional-only, as is the first parameter ofset_authorizer(),set_progress_handler()andset_trace_callback(). Calls that passed these the “wrong” way will fail.argparse: with both a short option and a single-dash long option, such asadd_argument('-f', '-foo'),destis now inferred from the long option ('foo', not'f'). Pass an explicitdestto keep the old behaviour.base64.urlsafe_b64decode(): input padding is no longer required. If you depended on the error for unpadded input, passpadded=True.unittest:assertWarns()andassertWarnsRegex()no longer swallow warnings that don’t match. Tests may start leaking warnings that were previously masked. Silence them with warning filters or catch them with an additionalassertWarns*().resource:resource.RLIM_INFINITYis now always positive. Passing the old negative value tosetrlimit()orprlimit()is deprecated.xml.etree.ElementTree.iterparse(): an unclosed iterator that opened a file now emits aResourceWarning. Close it explicitly or wrap it incontextlib.closing().mmap:mmap.resize()is removed on platforms without the underlying syscall, instead of raisingSystemError.
Removed in 3.15
Everything below had been deprecated for at least a release or two, so a run on 3.14 with warnings enabled should flag most of it.
ast: node constructors now raiseTypeErrorfor a missing required argument or an unknown keyword argument.typing: the undocumentedNamedTuple("Point", x=int, y=int)keyword syntax is gone, as is the zero-fieldTypedDict("TD")form,@no_type_check_decorator, andByteStringintyping.__all__. Use class-based syntax. See the type hints guide if you’re modernising, and run mypy to catch the rest.collections.abc.ByteString: removed from__all__(full removal is scheduled for 3.17).datetime.strptime(): raisesValueErrorfor a%dformat without a year directive.glob:glob.glob0()andglob.glob1()are gone. Useglob.glob()withroot_dir.http.server:CGIHTTPRequestHandlerand the--cgiflag are removed.threading: the C implementation ofRLockno longer accepts arbitrary positional or keyword arguments (deprecated in 3.14).importlib:Loader.load_module()is no longer called; implementexec_module(). Thepackageparameter ofimportlib.resources.files()is removed too, andzipimporter.load_module()with it.pathlib:PurePath.is_reserved()is removed. Useos.path.isreserved().platform.java_ver(),sre_compile/sre_constants/sre_parse,ctypes.SetPointerType(),types.CodeType.co_lnotab, and thewavegetmark()/setmark()/getmarkers()methods are removed.sysconfig: thecheck_homeparameter ofis_python_build()is removed.
If you maintain C extensions
Several long-deprecated C APIs are removed: PyWeakref_GetObject() and PyWeakref_GET_OBJECT (use PyWeakref_GetRef()), PyImport_ImportModuleNoBlock(), PySys_ResetWarnOptions(), and several PyUnicode_As*Encoded* and PyUnicode_As*Decoded* functions (use PyCodec_Encode() / PyCodec_Decode()). Several initialisation functions such as Py_GetExecPrefix() are removed in favour of PyConfig_Get(). The pythoncapi-compat project provides compatibility shims so one codebase can support older versions too.
Get ahead of 3.16
Things you can fix now while you’re in the code anyway. These are deprecated and scheduled for removal in 3.16:
- The
asynciopolicy system (get_event_loop_policy(),set_event_loop_policy(),DefaultEventLoopPolicyand friends). Useasyncio.run()orasyncio.Runnerwithloop_factoryinstead. See async/await explained for the modern patterns. asyncio.iscoroutinefunction(): useinspect.iscoroutinefunction().- Bitwise inversion on booleans (
~True,~False), which gives-2and-1. Usenot x. - The
array'u'format code: use'w'. - Custom
logginghandlers defined with astrmargument: define them withstreaminstead. - Undotted extensions in
mimetypes.MimeTypes.add_type(), which will raiseValueErrorin 3.16: use.ext.
A note on lazy imports
The headline new feature, explicit lazy imports (PEP 810), isn’t a porting concern: it’s opt-in via the lazy keyword, so nothing changes until you use it. One thing to know before you enable it globally with -X lazy_imports=all: a failing import is raised at first use rather than at import time, so a missing dependency can surface later than it used to.
A sensible upgrade order
- Make sure your tests pass on 3.14 with
-W error::DeprecationWarning. - Add
encoding="utf-8"to everyopen()that lacks one. - Add 3.15 to CI, and fix what the list above turns up.
- Check that your key dependencies publish 3.15 wheels before moving production. Package support for a new Python typically lags the release by weeks, and a tool like uv makes it cheap to test against a fresh interpreter.
- Wait for 3.15.1 or 3.15.2 for anything critical, as with any
.0release. Check the Python version end-of-life dates to see how long your current version stays supported, so you know how much time you have.
Frequently Asked Questions
Will upgrading to Python 3.15 break my code?
Most code runs unchanged. The changes most likely to affect real projects are the UTF-8 default encoding (for open() calls without an explicit encoding), a handful of removed deprecated APIs, and tighter signatures in sqlite3. Running your test suite on 3.15 with deprecation warnings turned into errors finds nearly all of it.
What is the biggest behaviour change in Python 3.15?
Python now uses UTF-8 as the default encoding regardless of the system’s locale (PEP 686). Code that calls open() without an encoding argument will read and write UTF-8, which only matters if your files were relying on a different platform default, typically on Windows.
How do I keep the old default encoding?
Pass encoding explicitly on every open() call, which also works on older Python versions. If you need a stopgap, UTF-8 mode can be disabled with the PYTHONUTF8=0 environment variable or the -X utf8=0 command-line option.
Do lazy imports change how my existing code behaves?
Not unless you opt in. Lazy imports use the new lazy keyword, or the -X lazy_imports option and PYTHON_LAZY_IMPORTS environment variable to apply them globally. Existing imports stay eager by default.