Python
Python is a general-purpose language whose standard interpreter runs Python code one thread at a time and leaves heavy number-crunching to compiled libraries. That lock, the GIL, is why four threads are rarely four times faster, why Python web servers run as several processes, and why a slow service needs a diagnosis before a fix: is it waiting, or computing? The page opens with twelve workloads and the tool that actually speeds each one up, then covers the GIL and the new build without it, asyncio, versions and the yearly release, types and packaging, web servers and their memory, five documented cases, and where it degrades.
- Current version
- 3.14 · 3.15 scheduled 1 Oct 2026
- Model
- One thread runs Python at a time
- Support
- 5 years: 2 of bug fixes, 3 of security
Waiting or computing? Twelve workloads and the tool that helps
Every Python performance question starts with the same diagnosis. Is the program slow because it is waiting — on a network, a disk, a database — or because it is computing? In a latency graph the two look identical. In Python they have opposite fixes, because the standard interpreter lets only one thread run Python code at a time. Waiting work spreads across threads well; computing work written in Python does not spread at all. Here are twelve common workloads, the diagnosis for each and what actually helps. Later sections cite the rows by number.
| # | Workload | Waiting or computing | Do more threads help? | What actually helps |
|---|---|---|---|---|
| 1 | 500 calls to an outside API | Waiting | Yes | asyncio with an async client, or a thread pool |
| 2 | A Django view that waits on the database | Waiting | Yes — but the web server decides how many | More server workers, or threads inside each (gthread) |
| 3 | Downloading or reading 10,000 files | Waiting | Yes | ThreadPoolExecutor |
| 4 | requests.get() or time.sleep() inside async def | Waiting — on the loop’s only thread | No: it stops every other coroutine | An async client, or asyncio.to_thread() |
| 5 | A pure-Python loop over 10 million rows | Computing, in Python | No | One array operation in NumPy, pandas or Polars, or a process pool |
| 6 | Parsing a 2 GB CSV file | Computing | Not with the csv module | A reader written in C or Rust — pandas, Polars, pyarrow |
| 7 | Multiplying large NumPy matrices | Computing, in C | Not needed — the library already uses every core | Nothing; cap the library’s threads when you also run several processes |
| 8 | Resizing images with Pillow | Computing, in C | Often — many operations release the lock | Threads, or processes for large batches |
| 9 | Model inference with PyTorch | Computing, in C++ or on a GPU | The library releases the lock itself | Batch the requests; one process per GPU |
| 10 | Pure-Python work that must use every core in one process | Computing, in Python | Only on the free-threaded build | python3.14t, if every extension supports it; processes otherwise |
| 11 | A command-line tool that takes two seconds to start | Importing modules | No | python -X importtime, then lazy imports (3.15) |
| 12 | An API where turning objects into a response takes most of the time | Computing, in Python | No | A compiled serialiser — orjson, or Pydantic 2, whose core is written in Rust |
The GIL, and the build without it
CPython, the standard interpreter, protects its internals with one lock — the global interpreter lock, or GIL. A thread must hold it to execute Python bytecodeinstructions for a virtual machineCompact instructions for a virtual machine rather than for a real processor — what the JVM, CPython, PHP’s OPcache, React Native’s Hermes and the Ethereum Virtual Machine run. A compiler produces it from source code ahead of time, so the runtime skips parsing; the virtual machine then interprets it or compiles the busy parts further into machine code.Full article →All terms. Threads are real operating-system threads and are scheduled normally, but only the one holding the lock runs Python. Every 5 milliseconds by default the holder is asked to let go, so the others get a turn; which thread runs next is the operating system’s choice. Start three threads to crunch numbers on a machine with three free cores, and you get the throughput of one core, plus the cost of switching.
The other half of the rule is what keeps threads useful. A thread that is waiting — on a socket, a file, a database answer — releases the lock while it waits, and so do well-written C extensions while they do long work: NumPy, pandas, PyTorch, the database drivers. That is rows 1–3 and 7–9. The lock exists because of how CPython manages memory. Every object carries a reference count, changed each time something starts or stops using it. Two threads changing the same count at once would corrupt it, and one big lock made every count safe at almost no cost to a program with a single thread.
The free-threaded build
Removing the lock means making every reference count and every built-in type safe some other way. PEP 703 described how, and it is being rolled out in phases. Python 3.13, in October 2024, shipped a separate experimental build without the GIL. Python 3.14, in October 2025, made that build officially supported, though still optional and still separate: you install python3.14t beside the ordinary interpreter. The price on single-threaded code is now "roughly 5-10%", according to the release notes, down from far more in 3.13. Python 3.15 adds a stable binary interface for free-threaded builds, so a library can ship one compiled package that works across versions. Making the free-threaded build the default is a separate decision, with no date.
The catch is the extensions. A C extension has to declare that it is safe without the lock. If you import one that does not, the free-threaded interpreter turns the GIL back on for the whole process and prints a warning — so a single old dependency quietly returns you to the standard behaviour. Before moving row 10 onto python3.14t, check every compiled dependency, not just your own code.
| Option | What runs in parallel | What it costs | Use it for |
|---|---|---|---|
| Threads, standard build | Waiting, and C code that releases the lock | Little; memory is shared | Rows 1–3 and 7–9 |
| Processes — a process pool, several server workers | Everything | An interpreter’s memory each; data copied between them | Computing in Python today |
Subinterpreters — concurrent.interpreters, 3.14 | Python code, one lock per interpreter | No shared objects; many extensions not supported yet | Isolated computing tasks inside one process |
Free-threaded build — python3.14t | Python code, across ordinary threads | About 5–10% on single-threaded code; every extension must support it | Row 10, once the dependencies are ready |
asyncio: one thread, and three ways to stall it
asyncio is the standard library’s answer to rows 1–3: an event loopone thread taking turnsA loop on a single thread that takes finished work from a queue — a reply that has arrived, a timer that has fired — and runs its callback, one at a time. It lets one thread juggle thousands of waits, and it stops for everyone while any single callback runs.Full article →All terms on one thread, running many coroutines that take turns. A coroutine — a function defined with async def — runs until it reaches an await on something that is not ready, hands control back to the loop, and is resumed when the answer arrives. Ten thousand connections can be in flight this way on a single thread. It is often reached for as a general speed-up, and it is not one. It overlaps waiting; it does nothing for computing. And it depends on every piece of code cooperating, which is where it breaks.
The first way to stall it is row 4. A call that blocks — requests.get(), time.sleep(), a classic database driver — never hands control back. The loop’s one thread sits inside it, and every other coroutine waits, however ready its own answer is. One library without async support can undo the whole design, and it is the most common reason an asyncio service ends up slower than the threaded one it replaced.
| Inside `async def`, this blocks the loop | Use instead |
|---|---|
requests.get() | httpx.AsyncClient or aiohttp |
time.sleep() | await asyncio.sleep() |
psycopg2 and other classic database drivers | psycopg 3 in async mode, asyncpg |
| An SDK with no async version | await asyncio.to_thread(call, …) — the call runs on a worker thread |
| Reading or writing a large file | asyncio.to_thread() as well |
| Heavy computation (rows 5, 6, 12) | loop.run_in_executor() with a process pool |
The second way loses work without any error. asyncio.create_task() starts a coroutine in the background, and the documentation carries a warning most people never read: "The event loop only keeps weak references to tasks. A task that isn’t referenced elsewhere may get garbage collected at any time, even before it’s done." A fire-and-forget task — send an email, write an audit record — can simply vanish. Keep a reference to every task, or start them inside an asyncio.TaskGroup (Python 3.11), which waits for all of them and cancels the rest if one fails.
The third way is a wait with no end. A coroutine awaiting a service that never answers holds its place in the loop forever, and with enough of them the service runs out of connections or memory. Wrap every outside call in asyncio.timeout() (also 3.11) or give the client its own timeout.
Versions: one a year, five years each
A new Python version comes out every October. It gets two years of bug fixes — 18 months for versions before 3.13 — then security fixes only, until five years after its release, and then nothing at all. Python 2 reached that point on 1 January 2020, after its end date had already been moved five years. The table lists what each recent version added that you will meet in code, and when its security fixes end.
| Version | Released | What you will meet in code | Security fixes until |
|---|---|---|---|
| 3.9 | Oct 2020 | dict merging with |, list[int] without an import, zoneinfo | Ended Oct 2025 |
| 3.10 | Oct 2021 | match statements, X | None in type hints, clearer error messages | Oct 2026 — this month |
| 3.11 | Oct 2022 | 1.25× faster on average, except*, asyncio.TaskGroup, tomllib | Oct 2027 |
| 3.12 | Oct 2023 | type aliases and generic syntax, f-strings that may reuse quotes, distutils removed | Oct 2028 |
| 3.13 | Oct 2024 | Experimental free-threaded build and JIT, a new interactive shell, cgi and telnetlib removed | Oct 2029 |
| 3.14 | Oct 2025 | Free-threaded build supported, template strings t"…", annotations evaluated on demand, concurrent.interpreters, Zstandard | Oct 2030 |
| 3.15 | Scheduled 1 Oct 2026 | UTF-8 by default, explicit lazy imports, frozendict, the Tachyon sampling profiler | Oct 2031 |
Types and packaging: let the machine check
Python checks types when it runs, not when you save. That is what makes it quick to write and what makes a large code base risky: a misspelled attribute is an AttributeError in production rather than a red underline. Type hints are the answer the language settled on — annotations the interpreter ignores and a checker reads to find the mistake before it ships. mypy is the original checker, Pyright is the one behind most editors, and ty, from Astral, appeared as a beta in December 2025. Hints are gradual: annotate a module’s boundaries today and its internals next month, and raise the checker’s strictness on purpose.
Where data enters the system, hints alone are not enough: a hint says what should arrive, not what did. Pydantic closes that gap by checking and converting real data against the annotation at runtime, which is why it sits under FastAPI and most modern Python APIApplication Programming Interface — contract between programsThe rules one program follows to call another: which requests it may send, in what shape, and what comes back. To the caller it is a contract — everything behind it stays a black box, which is exactly what lets the other side be rewritten without breaking you.Full article →All terms code. Run the static checker in the build, validate at the boundary, and let the types describe everything in between.
Packaging is the other thing to get right early, because its failures are silent. Every project gets its own virtual environment — modern Linux distributions refuse pip install into the system Python outright, with an "externally-managed-environment" error. Dependencies are declared in pyproject.toml, and the exact versions that were resolved go into a lock file that is committed. Without one, two developers and the build server run three different sets of dependencies, and the difference shows up as a bug nobody can reproduce. Each tool had its own lock format; since March 2025 there is a standard one, pylock.toml.
| Job | Classic tools | Common in 2026 |
|---|---|---|
| Install, resolve and lock dependencies | pip with pip-tools, Poetry | uv, Poetry |
| Virtual environments and Python versions | venv, virtualenv, pyenv | uv |
| Lint and format | flake8, isort, Black | Ruff |
| Check types | mypy | mypy, Pyright, ty (beta) |
uv, Ruff and ty come from one company, Astral, and are written in Rust, which is most of the reason they are ten to a hundred times faster than the tools they replace. In March 2026 Astral announced that it had agreed to join OpenAI and said the tools would stay open source. That is a reason to lean on the standards — pyproject.toml, pylock.toml, ordinary virtual environments — rather than on one tool’s private features, so that switching stays cheap.
Two indexes, one name
Companies often publish internal packages to a private index and add it with --extra-index-url, next to the public one, PyPI. The trap is how pip chooses. It does not prefer your index. It "will combine the candidate versions from all indexes, and select the best version from the combined set", in the words of the uv documentation. So if anyone publishes a package with the same name on PyPI and a higher version number, pip installs theirs — and runs its code. This is called dependency confusion. uv’s default is the opposite: the first index that has the name is the only one it uses. That default exists because of the case shown in the diagram.
Django, FastAPI, and the server underneath
Two frameworks cover most server-side Python, and they are built on opposite assumptions rather than being competitors at different levels of quality. Django gives you a complete application — ORMObject-Relational Mapping — objects instead of SQLA library that hands you language objects where the database has table rows, so you write little or no SQL. The convenience costs control: the query it generates can be far heavier than the one you would have written yourself, and you only find out under load.Full article →All terms, migrations, authentication, an admin interface, forms, templates — and expects you to build inside it. FastAPI gives you routing, validation and documentation, and expects you to choose everything else. Flask is still a reasonable minimal choice; most new API work goes to FastAPI because validation and the generated OpenAPIa machine-readable API descriptionA single file describing an HTTP API in a form machines read: routes, parameters, response shapes. Documentation, client libraries and test stubs are generated from it, which is what stops the description from drifting away from the code.Full article →All terms schema come from the same type hints you were writing anyway.
Django
- Batteries included — admin, auth, ORM and migrations arrive on day one.
- Synchronous at heart; async views exist, and the ORM is only partly async.
- 6.1 is current; 5.2 is the LTSLong-Term Support — a release patched for yearsA release line that keeps receiving security and critical fixes for a published period after newer versions ship, but no new features. It lets a team upgrade on its own schedule — until the end date on the support table, after which the next vulnerability has no fix at all.All terms, patched until April 2028.
- The risk is a "fat model" that grows until business rules can only be tested through views.
FastAPI
- Async-first, built on ASGI — a natural fit for services that mostly wait on other services.
- Validation and OpenAPI documentation are generated from your type hints.
- Still versioned 0.x, so read the release notes before every upgrade.
- The risk is assembling your own stack — ORM, migrations, auth, admin — one decision at a time.
Django’s release model is changing too. Today a feature release comes out every eight months and only every third one is a long-term release. The next, 6.2, is due in April 2027 and supported to April 2030. From January 2028 Django moves to one release a year, numbered by the year — Django 2028 — and every release gets the same three years of support.
Neither framework runs itself in production. A Django application runs under a WSGI server such as Gunicorn; a FastAPI application under an ASGI server such as Uvicorn, often as Gunicorn workers. That server’s worker count is the real limit on concurrency, and because of the GIL the useful setting is several worker processes rather than many threads in one. Gunicorn’s documentation suggests starting from "(2 × CPUCentral Processing Unit — the processorThe processor that executes a program’s instructions. Calling a task “CPU-bound” means it is limited by computation rather than by waiting on disk or network — and the two need opposite fixes, so the distinction is worth making early.All terms cores) + 1" and warns that it "typically needs only 4–12 workers". The count is usually bounded by memory rather than by cores — which makes it worth knowing how much memory the workers actually share.
Why workers share less memory than they should
A pre-forking server loads the application once in a master process and then forks it into workers. On Linux a forked child starts by sharing every memory page with its parent; a page is copied only when one side writes to it. This is copy-on-write, and it should make twenty workers cost little more than one. Python defeats it in two ways. Reading an object changes its reference count — a write to the object’s header — so merely using shared objects copies the pages they live on. And a garbage-collection pass touches every tracked object, so one pass can copy most of the shared memory at once.
Two fixes came out of exactly this problem. gc.freeze(), added in 3.7, moves every object that exists at the moment of the call into a permanent generation the collector never visits again: call gc.disable() early in the master, gc.freeze() just before forking, and gc.enable() in each worker. And since 3.12 some objects are immortal — None, small integers, interned strings — so their reference counts no longer change. To see whether it matters for you, compare a worker’s shared and private memory in /proc/<pid>/smaps_rollup a minute after start and an hour later.
Five cases, and what each one was missing
Each case is documented by the people it happened to — Python’s own release records, engineering blogs, the project’s incident notice, the security advisory. Between them they cover the language, memory, packaging, data files and the money behind the interpreter.
| Case | What happened | What was missing |
|---|---|---|
| Python 2 to 3, 2008–2020 | A break in how text and bytes work took twelve years to finish | A way to run one code base on both versions from day one |
| Instagram, Jan 2017 | Workers lost a third of their shared memory within seconds of starting | Garbage collection that leaves the parent’s objects alone |
| torchtriton, Dec 2022 | A package on PyPI with the name of a private dependency stole files from users of nightly builds | An installer that keeps to the index a name belongs to |
| Pickled models, 2024–2025 | About a hundred model files on Hugging Face ran code when loaded | A weights format that cannot run code |
| Faster CPython, 2021–2025 | A plan for a five-fold speed-up, 1.25× delivered, then the team’s funding ended | Funding not tied to one employer |
The first case is the longest migration a major language has asked of its users. Python 3.0, released on 3 December 2008, changed what a string is: text became Unicode and bytes became a separate type, so code that mixed them — most code that touched files or the network — had to be rewritten, and it could not easily run on both versions. Python 2.7’s end was set for 2015 and then moved five years; support stopped on 1 January 2020, and the last release, 2.7.18, came out in April 2020. Dropbox, whose desktop client held more than a million lines of Python, started its move in 2015 and finished in 2018 by shipping both interpreters side by side and switching users over gradually. What eventually made such moves possible — code written to run on both, then type checkers to find what broke — did not exist at the start. A language can change its foundations once a generation, and pays for it in years.
The third case is the diagram in the packaging section, for real. PyTorch’s nightly builds depended on a package called torchtriton, published on PyTorch’s own index. Until December 2022 no package of that name existed on PyPI. Then someone uploaded one, and between 25 and 30 December 2022 anyone who installed the nightly build on Linux with pip got the PyPI copy instead — "Since the PyPI index takes precedence", as PyTorch’s notice put it. Importing it ran a binary that read the machine’s name, environment variables, /etc/passwd, everything under ~/.ssh and the first thousand files in the home directory, and sent them out through DNS queries. PyTorch renamed the dependency to pytorch-triton, registered that name on PyPI so nobody else could, and removed the affected builds. Stable releases were not affected.
The fifth case is about who pays for the interpreter to get faster. In 2020 Mark Shannon published a plan to make CPython about five times faster in four stages of roughly 50% each, and from 2021 Microsoft funded a small team, with Guido van Rossum, to carry it out. The work was real: 3.11 was, by its release notes, 1.25 times faster on average, and an experimental JITJust-In-Time compilation — compiling while runningCompiling code while the program is already running: the runtime interprets it at first, watches which parts are hot, and turns those into machine code on the fly. That is why the first seconds after start are slower than the minutes that follow.All terms followed in 3.13. In May 2025 Microsoft cancelled its support for the project and laid off most of the team, on the eve of the language summit at PyCon. The improvements already merged stay in every release; the plan’s later stages lost their main engineers. A language used by millions still depends, for this kind of work, on whether one company keeps a few people on it — and the speed you plan for should be the speed you have measured, not the speed on a roadmap.
Where it degrades
Python is forgiving to write and unforgiving to guess about. Almost every production problem on this page is a diagnosis skipped — a row of the first table treated as if it were another — or a check the machine could have done and was not asked to:
- Threads added to speed up computing in Python, which the lock guarantees will not help (rows 5 and 10).
- A blocking call inside an async handler, which stops the loop for every other request (row 4).
- Background tasks with no reference kept, which can disappear without an error.
- Element-by-element loops over arrays where one library call would do the same work in compiled code.
- Shared state between threads protected by nothing but the belief that the GIL protects it.
- Type hints written and never checked, which are worse than none because they are believed.
- Dependencies without a lock file, or a private index added next to PyPI with
--extra-index-url. pickle.loadortorch.loadon a file someone else produced.- Worker counts set by habit rather than by measured memory, with copy-on-write quietly lost.
- A version past the last column of the version table — from November 2026, anything older than 3.11.
The honest limit is the interpreter’s speed on code written in Python itself. For work whose hot loop cannot be handed to a library — a custom simulation, a parser, a per-request algorithm — Python is tens of times slower than a compiled language, and neither threads nor asyncio change that. The free-threaded build lets that code use more cores, not run faster on each. Python wins where the slow part is waiting, or where someone has already written the hot loop in C, C++ or Rust; for everything else the honest answer is to measure, and to move the hot loop rather than the whole service.
The practical check takes five minutes. Run python --version in the production image and find the row in the version table. Look for a committed lock file and for --extra-index-url in the build. Run the tests once with PYTHONASYNCIODEBUG=1 and read what it logs. And check the server’s worker count against the memory each worker really uses, not against the number of cores.
When to use it
Use it when
- Data work, analytics and machine learning, where the ecosystem has no serious competitor.
- APIs and internal services whose time goes on waiting — FastAPI with async fits that shape exactly.
- Products that need an admin interface, accounts and a data model quickly — that is what Django is.
- Automation, scripting and glue between systems, where readability matters more than throughput.
Avoid it when
- Computing in pure Python that must use every core of one process — unless the free-threaded build and all its extensions fit.
- Latency-critical paths measured in microseconds, where interpreter overhead is the dominant cost.
- Large code bases with no type checking in the build, where the missing compiler is felt every release.
- Loading model or data files from untrusted sources in a format that can execute code.
Sources and method
What this page stands on
- 23 sources
- 2 documented failures
- Checked October 13, 2026
Practices and where the terms come from
waiting or computing: threads, the GIL, asyncio, processes and versions- Documentationsys — System-specific parameters and functions (setswitchinterval) (opens in a new tab)docs.python.org
- DocumentationPython support for free threading (opens in a new tab)docs.python.org
- DocumentationCoroutines and tasks (opens in a new tab)docs.python.org
- Documentationgc — Garbage Collector interface (opens in a new tab)docs.python.org
- Documentationpickle — Python object serialization (opens in a new tab)docs.python.org
- DocumentationWhat’s New In Python 3.11 (opens in a new tab)docs.python.org
- DocumentationWhat’s new in Python 3.14 (opens in a new tab)docs.python.org
- DocumentationWhat’s new in Python 3.15 (opens in a new tab)docs.python.org
- DocumentationStatus of Python versions (opens in a new tab)Python Developer’s Guide
- StandardPEP 703 – Making the Global Interpreter Lock Optional in CPython (opens in a new tab)Python Software Foundation
- StandardPEP 683 – Immortal Objects, Using a Fixed Refcount (opens in a new tab)Python Software Foundation
- Documentationuv — Package indexes (opens in a new tab)Astral
- DocumentationGunicorn — Design (opens in a new tab)gunicorn.org
- DocumentationDownload Django — supported versions (opens in a new tab)djangoproject.com
The failures this page learns from
torchtriton and pickled models, in “Five cases”- PostmortemCompromised PyTorch-nightly dependency chain between December 25th and December 30th, 2022 (opens in a new tab)PyTorch · Dec 31, 2022
- ResearchData Scientists Targeted by Malicious Hugging Face ML Models with Silent Backdoor (opens in a new tab)JFrog · Feb 2024
- DocumentationPyTorch 2.6.0 Release (opens in a new tab)GitHub · pytorch/pytorch · Jan 29, 2025
- Documentationtorch.load with weights_only=True RCE (CVE-2025-32434) (opens in a new tab)GitHub Security Advisory · pytorch/pytorch · Apr 18, 2025
Python 3, Instagram and Faster CPython
the other three cases- StandardPEP 373 – Python 2.7 Release Schedule (opens in a new tab)Python Software Foundation
- ArticleMax Bélanger, Damien DeVille — How we rolled out one of the largest Python 3 migrations ever (opens in a new tab)Dropbox · Sep 25, 2018
- ArticleChenyang Wu, Min Ni — Dismissing Python Garbage Collection at Instagram (opens in a new tab)Instagram Engineering · Jan 17, 2017
- DocumentationMark Shannon — faster-cpython: plan (opens in a new tab)GitHub
- ArticleMicrosoft’s latest layoffs hit software engineers hard (opens in a new tab)The Register · May 16, 2025
Researched with Claude. Finding and cross-checking the primary sources was done with Claude (Anthropic). Selection, writing and fact-checking: the Anatomy of IT Product editors.
Found this useful?
Share it with someone who is working on the same problem.