Python 3.13 Is Here: A Real REPL, an Optional GIL, and Nineteen Modules That Vanished
Python 3.13.0 shipped on October 7, and it’s an unusual release. Most of the headline items are things you can’t use in production yet, and the item that will actually change your Tuesday is a pile of deletions. That makes it easy to get excited about the wrong thing and then hit a build failure a week later from a module you forgot you imported. Here’s my read, based on what the core team published themselves.
The REPL is finally worth using
The change you feel immediately is the new interactive shell. What’s New In Python 3.13 says it’s built on code from the PyPy project, and it’s the default when you start Python from an interactive terminal.
Multiline editing works and history survives across lines, so you can arrow back up into a function you defined three prompts ago and fix the typo instead of retyping it. You can type exit or quit or help on their own, without parentheses, and they do the obvious thing. Prompts and tracebacks come out in color by default. There’s an interactive help browser on F1 with its own command history, history browsing on F2 that skips output and the >>> and continuation prompts so you get just the code you typed, and a paste mode on F3 for dropping in a larger block without the auto indentation fighting you.
If any of that gets in the way, set PYTHON_BASIC_REPL and you get the old shell back. Color is separately controlled through PYTHON_COLORS and NO_COLOR, which is the knob to reach for if your tooling captures interpreter output and chokes on escape codes.
Error messages keep chipping away at the confusing cases
Tracebacks are colorized in the terminal by default now, controlled by PYTHON_COLORS as well as the canonical NO_COLOR and FORCE_COLOR, and two specific messages got better in ways that will save somebody an afternoon. The first covers the classic mistake of naming your script after a module you import. Save a file as random.py, import random inside it, and you used to get a baffling AttributeError about random having no attribute randint. Now the message points out that your file shares a name with the standard library module and that the import system gives it precedence, with a similar hint for third party module names. Everyone has burned time on this one.
The second is keyword argument suggestions. Call "text".split(max_split=1) and the TypeError now ends with a suggestion that you meant maxsplit, which extends the “did you mean” help from earlier releases to the case where you half remember an API.
Free threading, and what experimental really meant
This is the one everyone wants to talk about. PEP 703 made the global interpreter lock optional, and 3.13 ships the first official build of it.
The important detail is that it’s a separate interpreter binary, usually called python3.13t. You get it from the official Windows and macOS installers by choosing the free threaded option, or by building from source with --disable-gil. It is not a runtime flag on your normal Python, and it is not ABI compatible with the standard build, which is why the ABI tag carries a t and why wheels have to be built separately for it. You also need pip 24.1 or newer to install C extension packages into it.
Checking what you have is easy. python -VV and sys.version say “experimental free-threading build” on the free threaded interpreter, and sys._is_gil_enabled() tells you whether the lock is actually off in the running process, which is a separate question because of how C extensions work. An extension that supports running without the GIL declares it with the Py_mod_gil slot, or with PyUnstable_Module_SetGIL() if it uses single phase init. Import an extension that does neither, and CPython turns the GIL back on for the whole process rather than risk running unsafe code in parallel, unless you forced it off with the PYTHON_GIL environment variable or -X gil=0. So “I’m on the free threaded build” and “my program is actually running without the GIL” are two different facts, and sys._is_gil_enabled() is how you tell them apart.
The docs are blunt about the cost: expect some bugs and a substantial single threaded performance hit. PEP 703’s own pyperformance numbers for its reference implementation put the execution overhead around 5 to 6 percent for single threaded programs and 7 to 8 percent with multiple threads, mostly from biased reference counting. Your real workload will differ, but the direction is not in doubt: you trade single threaded speed for the ability to use cores.
The word experimental is doing real work here. The Steering Council’s note at the top of PEP 703 says it accepted the PEP on the proviso that the rollout be gradual and break as little as possible, and that changes can be rolled back if they prove too disruptive, including potentially rolling back all of PEP 703 entirely. That is an unusually explicit escape hatch for an accepted PEP. If you maintain a library with a C extension, this is your window to get it building and tested against the free threaded ABI. If you ship a service, it’s a thing to prototype, not a thing to deploy.
The JIT you almost certainly can’t use
PEP 744 added a basic just in time compiler, and it isn’t in any normal build. You configure CPython yourself with --enable-experimental-jit, which brings a build time dependency on LLVM but no runtime dependency, because the templates are generated when you build. The technique is copy and patch: hot Tier 1 bytecode gets translated into an internal micro op representation called the Tier 2 IR, optimized, then stitched into machine code from those templates.
The flag takes values: yes builds and enables it, yes-off builds it but leaves it off unless you set PYTHON_JIT=1, and interpreter runs the Tier 2 interpreter without generating machine code, which exists mainly for debugging the optimizer. On Windows it’s PCbuild/build.bat --experimental-jit.
Do not read this as free speed. PEP 744 says plainly that the JIT is currently about as fast as the existing specializing interpreter, that it isn’t yet a clear win when always enabled given the extra memory and build dependency, and that until it stops being experimental it should not be used in production and may be broken or removed at any time without warning. The bar for graduating is a meaningful improvement on a popular platform, realistically around 5 percent, plus a Steering Council decision that enabling it is worth the maintenance cost. Treat it as groundwork merged so it can be improved in public.
The dead batteries are really gone
Here’s the part that breaks code. PEP 594 proposed removing nineteen standard library modules that were obsolete, unmaintained, or built for formats nobody ships anymore. They were deprecated in 3.11 and in 3.13 they’re gone: aifc, audioop, chunk, cgi, cgitb, crypt, imghdr, mailcap, msilib, nis, nntplib, ossaudiodev, pipes, sndhdr, spwd, sunau, telnetlib, uu and xdrlib.
The ones I’d expect to actually hurt are cgi, crypt, telnetlib and pipes. For cgi, the docs point at urllib.parse.parse_qsl for GET and HEAD query strings and the email.message module or the multipart library for POST and PUT bodies, since the email package implements the same MIME rules. For crypt, which turns up in anything touching Unix password hashes, they point at hashlib for a plain hash, or bcrypt, passlib and argon2-cffi for the real thing. telnetlib is still quietly driving network gear in a lot of places, and the pointer there is telnetlib3 or Exscript. pipes becomes subprocess.
The same release removed the 2to3 tool and the lib2to3 module, tkinter.tix, locale.resetlocale, the typing.io and typing.re namespaces, and configparser’s undocumented LegacyInterpolation. If you still have a lib2to3 based codemod in your tooling, it stops working the moment your CI image moves to 3.13.
What I’d actually test before moving
Grep first, run later. Searching your repo and your tooling for the nineteen removed module names, plus lib2to3, finds most of the breakage in a few minutes and costs nothing.
Then read the quieter behavior changes in the porting notes. pathlib.Path.glob and rglob with a pattern ending in ** now return files as well as directories, and a trailing slash gets the old directories only behavior back. That one is dangerous precisely because it doesn’t raise: a cleanup script that used to walk directories now walks files too. The mode attribute on gzip.GzipFile is now a string rather than an integer, getpass.getuser raises OSError for any failure instead of ImportError or KeyError depending on platform, and functools.partial used as a method emits a FutureWarning. PEP 667 also changed locals() in optimized scopes to return an independent snapshot on each call, which matters if you own a debugger, a tracer, or something clever with exec.
Check your platform floor too. The minimum macOS version moved from 10.9 to 10.13, and Emscripten is no longer officially supported.
My advice for most teams is to install 3.13 locally, enjoy the REPL, grep for removed modules now, and let production wait. PEP 719 says bugfix releases come roughly every two months, so a 3.13.1 should land around December, and the first couple of point releases after a major Python are where the sharp edges get filed off. Waiting costs less than usual here, because the two headline features ship with the core developers themselves saying not to run them in production yet, and 3.13 is the first release with the longer support window of two years of full support followed by three years of security fixes. It isn’t going anywhere.