Deep Engineering
Search
Expert·Published·3.13 · 3.14 · 3.15·30 MIN

GIL: not “Python can't do threads,” but “a thread waits exactly as long as you asked”

The switch interval is a setting that works: the median wait follows it, and at 5 and 50 ms it is off by a few percent. Until the case arrives where it does not work at all — and that case is measured in hundreds of milliseconds.

Full technical treatment

TL;DR

  • The GIL does not “slow threads down.” It makes them wait their turn, and the length of the wait is set by sys.setswitchinterval. Measured: at a 5 ms interval the median wait is 5.279 ms, at 50 ms it is 50.335 ms, at 0.5 ms it is 0.717 ms. At the top two values the miss is a few percent; at the bottom one it is already 43%: there the switch itself starts to cost something.
  • Exactly up to the moment the running thread enters one long operation. Then the interval means nothing: the same wait becomes 167.8 ms on a big-integer multiplication and 379.8 ms in the worst case on sorted(). This is not a bug — it is written in the comment next to the constant itself.
  • “Threads in Python are useless” is a statement about pure bytecode and about nothing else. Measured on the same two cores: pure Python — ×0.97, time.sleep — ×7.86, zlib.compress — ×2.04.
  • Free-threaded builds are neither a hypothesis nor an experiment: PEP 779 made them officially supported in 3.14. But they did not become the default, and the PEP says so directly — the decision about Phase III is “left for a future PEP”.
  • The cost of the free-threaded build is real and shows up more sharply on microbenchmarks than on the general benchmark suite. And it has a non-obvious part: reading an object created by another thread costs more even when there is no contention at all. Measured: +15.4% with no second thread whatsoever.

Why know this?

Because the GIL comes up in interviews, and the right answer there is usually wrong.

“The global interpreter lock keeps threads from running in parallel, so CPU-bound work needs multiprocessing” is a memorized formula, not understanding. It does not explain why requests works perfectly well across twenty threads, why numpy computes on every core, and why your web server sometimes produces responses of a hundred or two hundred milliseconds out of nowhere — when there is nothing there that takes that long to compute.

Below are four things, each of them taken off a running interpreter:

  1. what sys.setswitchinterval affects (the answer is more direct than it looks);
  2. when that setting affects nothing at all;
  3. the cases where the GIL takes no part in what is happening;
  4. what exactly changed with free-threaded builds — and what did not.

All timings were taken on CPython 3.13.7, in two builds of the same patch release: the ordinary one and one built with --disable-gil (the single exception is the table of getswitchinterval() values below, which comes from six different builds). Machine: Linux x86_64, 2 cores. Two cores is an important detail: every “speedup” below hits a ceiling of ×2, and nowhere do I pass that off as a general law.

The mental model

A meeting in a conference room. Many people, one microphone, and the rule is simple: whoever holds the microphone speaks.

This is not “people speak slowly.” Everyone speaks at ordinary speed. But if eight people each need to say one sentence, eight sentences take eight turns, not one — no matter how many chairs the room has.

The rule has three consequences, and all three are testable:

  • There are rules of order. The microphone changes hands roughly once every five milliseconds. “Roughly” is the operative word: there is an agreement, there is no enforcement.
  • There is an exception. While a person is not speaking but listening, or digging a piece of paper out of a bag, they hand the microphone over. Waiting does not hold the queue.
  • There is a hole in the rules. If someone starts reading a long document aloud, there is nobody to take the microphone away mid-paragraph. The rules say “five milliseconds,” reality says “until they finish reading.”

The rest of the article is these three consequences translated into numbers.

What the GIL literally is

The definition in the Python glossary describes a mechanism, not a metaphor: it is the means by which the CPython interpreter ensures that only one thread executes bytecode at a time. The same entry says why: it makes the object model, including types such as dict, implicitly safe against concurrent access.

And that same glossary paragraph carries two caveats that usually get skipped:

  • some extension modules are “designed so as to release the GIL when doing computationally intensive tasks such as compression or hashing”;
  • “the GIL is always released when doing I/O”.

That is, half of the claim “Python can't do parallelism” is refuted inside the definition of the GIL itself. We will come back to this in the third section.

The switching itself lives in Python/ceval_gil.c. So does the constant:

C
#define DEFAULT_INTERVAL 5000

Five thousand microseconds, that is, 5 ms. This is exactly the value sys.getswitchinterval() returns, and it is the same in every build checked:

Buildsys.getswitchinterval()
3.10.200.005
3.11.150.005
3.12.30.005
3.13.130.005
3.14.0rc20.005
3.13.7, no-GIL build0.005

The last row deserves a separate look: in a build where the GIL is off, the setting for the GIL switch interval still exists and still returns five milliseconds. Whether it affects anything there is a question with a measured answer below.

First: the interval works literally

Let's run the experiment that answers the question “what does setswitchinterval actually change.”

Two threads. The first is “greedy”: it computes in a loop and never gives up the interpreter voluntarily. The second is “quick”: in a loop it calls time.sleep(0), that is, yields control and immediately asks for it back, and records how much time passed. What it records is the time spent waiting its turn.

PYTHON
def probe():
    while not stop.is_set():
        t0 = time.perf_counter()
        time.sleep(0)                     # release the GIL and ask for it back
        lags.append((time.perf_counter() - t0) * 1000)

Median 5.279 ms, p95 5.408 ms, maximum 9.020 ms over 283 samples. The default value; the setting itself has been available since 3.2. The median wait is 5.279 ms at a 5 ms interval, a miss of 5.6 %. The setting works roughly as its name promises.

CPython 3.13.7, Linux x86_64, 2 cores. Blue marks the runs where the greedy thread performs many short operations; red, the ones where it performs a single long operation. The scale is shared, which is why the first three bars are barely visible next to the last two: that is exactly the difference between “the setting works” and “the setting has nothing to do with it”.

The first three columns show a relationship visible without any statistics:

IntervalMedian waitp95Samples
0.5 ms0.717 ms0.858 ms2026
5 ms (default)5.279 ms5.408 ms283
50 ms50.335 ms50.636 ms30

At 5 and 50 ms the median diverges from the setting by a few percent: +5.6% and +0.67%. At 0.5 ms the divergence is already +43% — and that too is a result, not measurement error: the more often control changes hands, the larger the share of the timeslice the handoff itself takes. The setting defines a lower bound on the wait, not an exact value.

From this follows a practical conclusion that is rarely stated outright: the GIL does not take CPU time away from you — it adds latency. If you have a threaded web server and one endpoint computes something heavy in pure Python, every other request picks up an addition to its latency on the order of the switch interval. The CPU may be idle the whole time.

The same thing is visible from the other side — if you look not at waiting but at holding.

thread 0
thread 1
0 ms15 ms30 ms45 ms60 ms

55 segments over 283 ms, median 5.127 ms. The default value. The median segment is 5.127 ms at a 5 ms interval, a miss of 2.5 %. Yet one segment in this window lasts 10.3 ms, twice as long.

A 60 ms window from a run of tmp/gil-timeline.py on CPython 3.13.7, Linux x86_64, 2 cores. Ownership is reconstructed from the timestamps the threads record themselves; the step between marks is tens of microseconds.

The median holding stretch at the default interval is 5.127 ms. The match with the setting is almost literal.

But the same window contains a stretch 10.3 ms long — two timeslices in a row. And that is not measurement error, it is the entrance to the second part of the story.

Second: the interval guarantees nothing

Next to DEFAULT_INTERVAL in the source there is a caveat worth reading in full: the mechanism “encourages a defined switching period, but doesn't enforce it since opcodes can take an arbitrary time to execute”.

Encourages, but does not enforce — because a single operation can run for arbitrarily long.

The sys.setswitchinterval documentation says the same thing in different words: the value sets the ideal duration of a timeslice, the “actual value can be higher, especially if long-running internal functions or methods are used”, and — in a sentence of its own — “The interpreter doesn't have its own scheduler”.

Let's check. Same experiment, same 5 ms interval, but the greedy thread now performs not a million short operations but one long one:

PYTHON
A = 7 ** 500_000        # 1,403,678 bits
B = 11 ** 500_000       # 1,729,716 bits
 
def one_big_mul():
    A * B               # one operation, nowhere to interrupt it
What the greedy thread doesMedian waitMaximum
many short operations5.293 ms6.598 ms
one big-integer multiplication167.756 ms217.305 ms
one sorted() over 2,000,000 elements185.619 ms379.826 ms

The median of the third row is thirty-five times the median of the first. The worst sample in that same row is 379.826 ms, that is, seventy-six times the configured interval of 5 ms. The interval itself was never changed.

The mechanism here is simple, and that is what makes it unpleasant. The request to hand over control is raised as a flag, and the flag is checked by the interpreter loop — between operations. Inside a single operation there is nobody to check it: sorted() goes into C code and returns from there when it is done. There is no “preemptive multitasking” at this level, and sys.setswitchinterval does not add any.

Five milliseconds is not a guarantee of latency. It is a guarantee that the interval alone will not make you wait longer. Everything else depends on what your neighbor has already entered.

The practical trace of this observation: if somewhere in your threaded server there is a sorted() over a large list, a regular expression over a megabyte of text, or arithmetic on big integers, lowering switchinterval will not help. The only thing that helps is moving it out of the thread: into a process, into a queue, into a library that releases the GIL.

Third: when the GIL has nothing to do with it

Now to the caveats from the glossary. Three kinds of work, two builds, the same code.

What the program doesBuild with the GILBuild without the GIL
Pure Python: arithmetic in a loop
4,000,000 iterations of x += i * i, split between the threads
0.1516 s0.1559 s
×0.97
the second thread gave nothing
0.1754 s0.0998 s
×1.76
there is a speedup, but not a twofold one
Waiting: time.sleep(0.05) eight times
sequentially versus eight threads
0.4016 s0.0511 s
×7.86
the threads add up almost perfectly
0.4014 s0.0519 s
×7.74
exactly the same
C extension: zlib.compress
a buffer of 5,999,872 bytes; the second column does twice the work
0.0564 s0.0553 s
×2.04
twice the work in the same time
0.0579 s0.0591 s
×1.96
the same again, the free-threaded build adds nothing

Bytecode runs only with the GIL held. Two threads share a single right to execute — they divide the time instead of adding it up.

CPython 3.13.7, both builds from the same patch, Linux x86_64, 2 cores. Every value is the minimum of several runs of tmp/gil-measure.py: five for the arithmetic row, three for the other two. Clicking a row shows why it came out the way it did.

This table is worth reading by rows, not by columns.

Pure Python — ×0.97. Two threads did the same work in the same time — more precisely, almost three percent slower: switches are not free. This is the only row for which the phrase “the GIL gets in the way of threads” exists.

Waiting — ×7.86. Eight time.sleep(0.05) calls in sequence take 0.4016 s, eight threads take 0.0511 s. Zero cores are needed for this: the threads are not computing, they are waiting, and for the duration of the wait the GIL is released. This is exactly why “Python can't do threads” never stopped anyone from downloading a hundred pages at once.

zlib.compress — ×2.04. The comparison here is set up differently, and that matters: one thread compresses a 5,999,872-byte buffer in 0.0564 s, two threads compress a buffer of the same size each — and finish in 0.0553 s. Twice the work, the same time. Both cores are busy, and the GIL is held by nobody.

Why this works is written in the C API documentation. The Py_BEGIN_ALLOW_THREADS macro expands to

C
{ PyThreadState *_save; _save = PyEval_SaveThread();

and its counterpart Py_END_ALLOW_THREADS expands to PyEval_RestoreThread(_save); }. Between them, C code runs without holding the interpreter. The documentation even names specific modules: “the standard zlib and hashlib modules detach the thread state when compressing or hashing data”.

From this follows a rule worth keeping in mind when choosing a library: the question is not “is it Python or C” but “does this function release the GIL”. A C extension that honestly computes while holding the GIL is no better for threads than pure Python — it is simply faster on one thread.

Free threading: what is already true

It is easy to overdo this in either direction, so: by dates and by documents.

PEP 703, Sam Gross, status Final, decision October 24, 2023, Python 3.13. Adds the --disable-gil build flag and states the boundary outright: “The global interpreter lock will remain the default for CPython builds and python.org downloads”. That is, the very PEP that “removes the GIL” says the GIL remains the default.

PEP 779, Thomas Wouters, Matt Page and Sam Gross, status Final, decision June 16, 2025, Python 3.14. This is Phase II: the free-threaded build is officially supported, but still optional. In the What's New sections for 3.14 this is recorded in one line: “PEP 779: Free-threaded Python is officially supported”.

Phase III — making the free-threaded build the default — has not happened. PEP 779 says so itself: the decision is “very different, and we expect it will revolve around community support, willingness, and showing clear benefit. That's left for a future PEP”. At the time of writing there is no such PEP.

What is happening right now: 3.15 is at the release-candidate stage (rc1 came out August 4, 2026, the final is scheduled for October 1), and the changes there are about tooling, not about changing the default: PEP 803 adds a stable ABI for free-threaded builds (abi3t), the profiler gains a --mode gil option, and the PyGILState_* family has been soft-deprecated — with no plans for removal and with existing code kept working.

You can check where you are from the interpreter itself:

PYTHON
import sys, sysconfig
 
sys._is_gil_enabled()                        # False in a free-threaded build
sysconfig.get_config_var("Py_GIL_DISABLED")  # 1 in a free-threaded build, 0 in the ordinary one
sys.abiflags                                 # "t" in a free-threaded build

And — the promised answer to the question from the first section: what sys.setswitchinterval does in a build where the GIL is off. The answer is nothing. The same wait measurement, three different intervals:

IntervalMedian wait, GIL buildMedian wait, no-GIL build
0.5 ms0.717 ms0.067 ms
5 ms5.279 ms0.067 ms
50 ms50.335 ms0.067 ms

Three identical values in the right column are the substantive result. The setting survived, returns the same 0.005, and does not affect behavior: the queue whose length it governed no longer exists. The remaining 0.067 ms is the cost of time.sleep(0) itself and of the OS scheduler, not a wait for anything inside the interpreter.

One more detail worth knowing in advance: the free-threaded build can turn the GIL back on. The PYTHON_GIL=1 variable or the -X gil=1 option brings it back. Verified: PYTHON_GIL=1 python3.13t -c "import sys; print(sys._is_gil_enabled())" prints True. This is not a curiosity — it is how the free-threaded build lives with extensions that are not ready for it.

What the free-threaded build costs

This is where it is easy to lie with a number, so: first the source, then my measurements, then an explanation of the discrepancy.

The official HOWTO names the price outright: “The free-threaded build has additional overhead when executing Python code compared to the default GIL-enabled build”, and then the range on the pyperformance suite: “from about 1% on macOS aarch64 to 8% on x86-64 Linux systems”.

My measurements came out higher:

What was measuredWith GILWithout GILDifference
arithmetic in a loop, 6,000,000 operations0.2306 s0.2638 s+14.4%
reading list elements in a loop on the main thread, 3,000,000 times0.1124 s0.1642 s+46.1%

This is not a refutation of the official figure, and presenting it as “the overhead is actually 46%” would be a lie. The difference is explained by what was measured: pyperformance is a set of heterogeneous programs where the interpreter is busy with more than executing bytecode. My two tests are tight loops made up of exactly the operations that got more expensive in the free-threaded build. This is the worst case, not the average.

The correct phrasing sounds duller and is more useful: the price depends on what the program is doing, and on tight pure bytecode it is noticeably above the benchmark average.

In exchange, the free-threaded build gives something that never happens with the GIL at all: on the same two cores, pure Python across two threads gave ×1.76. Not ×2 — part of it is eaten by the same price as in the table above, plus the machine has two cores and something else is running on it.

The non-obvious part of the price

This part is not written in the documentation, and it does not get asked in interviews.

PEP 703 uses biased reference counting — an approach that rests on the observation that “most objects are only accessed by a single thread, even in multi-threaded programs”. An object has an owning thread, and reference-count operations from that thread take the fast path, while those from another thread take the shared one.

Let's check whether this is visible from the outside. The experiment is deliberately arranged so that there is no contention at all: there is exactly one worker thread. Only the origin of the data changes.

PYTHON
FOREIGN = make(0)                 # list created on the main thread
 
def worker_reads_foreign():
    loop(FOREIGN, 3_000_000)      # the objects are owned by another thread
 
def worker_reads_own():
    items = make(2)               # list created right here
    loop(items, 3_000_000)
BuildForeign objectsOwn objectsRatio
with GIL0.1117 s0.1104 s1.012
without GIL0.2057 s0.1783 s1.154

In the GIL build there is no difference — 1.2% is noise. In the free-threaded build, reading foreign objects costs 15.4% more, even though there is no second thread and nobody to contend with.

And if a second thread does appear and both read the same objects, this is what happens: two threads doing 2,000,000 accesses each completed the work in 0.6108 s against 0.1376 s for a single thread. That is, the two of them together are slower than one, and with twice the work the result is ×0.45. Across four runs it is ×0.45–0.52.

The same pair of measurements in the GIL build gives ×0.94–1.00 across the same four runs. The spread here is larger than the effect itself, and that is precisely the answer: with the GIL, two threads on shared objects win nothing, but lose nothing either — they simply stand in the queue.

There is one practical conclusion from these three rows, and it is not about the GIL: in a free-threaded build it matters who created an object and who touches it. State shared between threads goes from “awkward” to “expensive”, and “let's just run it on the free-threaded build and get a speedup” does not work as a strategy.

What to do about it

A short map of decisions, each tied to something measured above.

The work is waiting (network, disk, database). Threads already work, and have for a long time. Measured ×7.86 across eight waits. The GIL takes no part here. The free-threaded build adds nothing: ×7.74, the same within measurement error.

The work is heavy computation inside a library. Check whether it releases the GIL. zlib and hashlib do, and that is written in the documentation; for the rest, look at the scaling, as in the table above. If it releases the GIL, threads will give a speedup across cores with no free-threaded build at all.

The work is pure Python, and there is a lot of it. Processes (multiprocessing, concurrent.futures.ProcessPoolExecutor) have remained the working answer since 2008. The free-threaded build is the second answer, now officially supported, but with the caveats from the previous section.

The problem is latency, not throughput. This is where sys.setswitchinterval makes sense, and the table at the start of the article shows exactly what you buy and what you pay: reducing the interval tenfold cut the median wait by a factor of 7.4 (5.279 → 0.717 ms) and increased the number of switches by the same factor (283 → 2026 samples in the same time). Not tenfold — part of the gain is eaten by the handoff itself. But if the latency is created by one long operation, the setting is useless — that is section two.

And in general. It is more useful to think of the GIL not as “a limitation of Python” but as an explicit queue that has a setting. The queue is visible, it is measured in three lines of code, and it behaves predictably right up to a boundary that is also documented — in the comment next to the constant that sets the interval.

Common misconceptions

Claim

“The GIL makes threads in Python useless.”

Actually

Useless only for pure bytecode. Measured on one and the same two-core machine: arithmetic in a loop gives ×0.97, eight time.sleep(0.05) calls give ×7.86, zlib.compress gives ×2.04 (two threads compress a full buffer each and finish in the time of one). Two of the three rows are about the GIL taking no part in what is happening: on I/O it is always released, and zlib and hashlib detach the thread state while they work, which is written down in the C API documentation.

Claim

sys.setswitchinterval is a fine-tuning knob, and its effect is unpredictable.”

Actually

The effect is direct and reproducible. A thread that asks for control immediately waits roughly as long as specified: a median of 0.717 ms at an interval of 0.5 ms, 5.279 ms at 5 ms, 50.335 ms at 50 ms. At the top two values the miss is a few percent; at 0.5 ms it is already 43%, because there the cost of the handoff itself is noticeable. What becomes unpredictable is not the effect of the setting but the behavior of the neighbor: if it has entered one long operation, the setting does not apply at all.

Claim

“The interpreter switches threads every 5 ms.”

Actually

It raises a switch request after 5 ms, and that request is checked between operations. Inside a single operation there is nobody to check it. Measured with the interval held at 5 ms: while the greedy thread performs many short operations, the wait is 5.293 ms; on a single multiplication of integers of 1.4 and 1.7 million bits it is 167.756 ms; on a single sorted() over two million elements it is 185.619 ms, and 379.826 ms in the worst sample. In Python/ceval_gil.c, next to DEFAULT_INTERVAL, this is stated in plain words: the mechanism “encourages a defined switching period, but doesn't enforce it”.

Claim

“The GIL was removed in Python 3.13.”

Actually

What appeared in 3.13 was a separate build behind the --disable-gil flag, and PEP 703 itself states the boundary: the GIL remains the default for CPython builds and python.org downloads. The free-threaded build became officially supported in 3.14 (PEP 779) — that is Phase II, “supported but still optional”. Phase III, that is, changing the default, is explicitly left to a future document in PEP 779: “That's left for a future PEP”. No such PEP exists at the time of writing.

Claim

“The free-threaded build is just a faster Python.”

Actually

It is slower on one thread and not always faster on two. The official HOWTO names a range of 1–8% on the pyperformance suite; on tight loops the measured cost is higher — +14.4% on arithmetic and +46.1% on reading list elements, and that is the worst case, not the average. The speedup is real, though: the same computation across two threads on two cores gave ×1.76. But two threads reading the same objects gave ×0.45–0.52 across four runs — the two of them together are slower than one; in the GIL build the same runs give ×0.94–1.00, that is, neither gain nor loss.

Claim

“Without the GIL all objects become equal.”

Actually

No: an object acquires an owning thread. PEP 703 uses biased reference counting, resting on the fact that “most objects are only accessed by a single thread”. The difference is visible even without contention. An experiment with exactly one worker thread: reading objects created on another thread took 0.2057 s against 0.1783 s for its own — 15.4% more expensive. In the GIL build the same two cases differ by 1.2%, that is, they do not differ.

Claim

“The free-threaded build can't work with old extensions, so it can't be adopted gradually.”

Actually

It can: the GIL can be turned back on in it. The PYTHON_GIL=1 variable or the -X gil=1 option brings it back, and sys._is_gil_enabled() starts printing True — verified on 3.13.7t. In 3.15 this is joined by PEP 803: a stable ABI for free-threaded builds (abi3t), thanks to which an extension no longer has to be built separately for each version.

Knowledge check

Question 1 of 5

A threaded web server. One endpoint computes something heavy in pure Python. What happens to the other requests at default settings?

Sources & further reading

10 SOURCES

  1. Python glossary — global interpreter lock, free threading, free-threaded buildOfficial documentation. The definition of the GIL word for word, including the two caveats that half of this article is built around: extensions may release the GIL during heavy computation, and on I/O it is always released.https://docs.python.org/3/glossary.html#term-global-interpreter-lock
  2. Python/ceval_gil.c — DEFAULT_INTERVAL and gil_drop_requestCPython source code. #define DEFAULT_INTERVAL 5000 (microseconds) and the caveat next to it: the interval “encourages a defined switching period, but doesn't enforce it since opcodes can take an arbitrary time to execute”. CPython tag 3.14.0.https://github.com/python/cpython/blob/v3.14.0/Python/ceval_gil.c
  3. sys.setswitchinterval and sys.getswitchintervalOfficial documentation. “This floating-point value determines the ideal duration of the timeslices” and a direct warning: “The interpreter doesn't have its own scheduler”. The default value is not named in the documentation.https://docs.python.org/3/library/sys.html#sys.setswitchinterval
  4. C API — Thread State and the Global Interpreter LockOfficial documentation. What exactly Py_BEGIN_ALLOW_THREADS expands to, and the explicit statement that zlib and hashlib detach the thread state while compressing and hashing.https://docs.python.org/3/c-api/threads.html
  5. Python support for free threading (HOWTO)Official documentation. sys._is_gil_enabled(), the PYTHON_GIL variable and -X gil, and the named price of the free-threaded build: “from about 1% on macOS aarch64 to 8% on x86-64 Linux systems” on the pyperformance suite.https://docs.python.org/3/howto/free-threading-python.html
  6. PEP 703 — Making the Global Interpreter Lock Optional in CPythonPEP. Sam Gross, Python 3.13, status Final, decision October 24, 2023. This is the source of biased reference counting and of the flat statement that the GIL remains the default for CPython builds and python.org downloads.https://peps.python.org/pep-0703/
  7. PEP 779 — Criteria for supported status for free-threaded PythonPEP. Thomas Wouters, Matt Page, Sam Gross; Python 3.14, status Final, decision June 16, 2025. Phase II: officially supported, but still optional. About Phase III it is explicit: “That's left for a future PEP”.https://peps.python.org/pep-0779/
  8. What's New In Python 3.14Official documentation. The wording “PEP 779: Free-threaded Python is officially supported” — the moment the build stopped counting as experimental.https://docs.python.org/3/whatsnew/3.14.html
  9. What's New In Python 3.15Official documentation. PEP 803 (“abi3t” — a stable ABI for free-threaded builds), the --mode gil option of the Tachyon profiler, and the soft deprecation of the PyGILState family. Checked against the preliminary 3.15 documentation.https://docs.python.org/3.15/whatsnew/3.15.html
  10. PEP 790 — Python 3.15 Release SchedulePEP. Needed in order to state the status of 3.15 honestly at the time of writing: rc1 came out August 4, 2026, the final release is scheduled for October 1, 2026.https://peps.python.org/pep-0790/