skip to content

When is raising a library's requires-python floor justified, and what evidence decides it?

level: principalimportance: should knowfreq 35%

answer

  1. A cost question, not taste
  2. Upstream end-of-life is the unarguable boundary
  3. Check your dependencies' floors first
  4. Frozen, not broken
  5. Delete the shims in the same release

basics

~20 s

Raise it when the oldest supported interpreter costs more than it earns: upstream end-of-life passed, install share has collapsed, dependencies have already moved, and shims plus test-matrix cost are real. Evidence beats taste, and old users keep getting old releases.

solid answer

~40 s

Treat the floor as a contract with a cost. The inputs are upstream support windows — CPython releases annually and each line gets roughly five years, the last three security-only — the share of installs still arriving from that interpreter, whether your own dependencies have already raised their floors, and what the old release costs you in shims, half-exercised branches and matrix jobs. For a thumbnail-rendering library with a 340-case regression pack, one extra interpreter is a recurring bill, and the oldest job is usually the one producing the odd clock-skew artefact nobody wants to own. Raising the floor is not abandonment: `requires-python` makes the installer keep serving those users your last supporting release. What you owe them is a clear release note and a decision about whether that line still gets security fixes.

go deeper

for a junior

Know that a project declares an oldest supported interpreter and that it moves over time. You are not expected to run the decision, only to understand that dropping a version is deliberate rather than accidental.

for a middle

Be able to describe the mechanics you would rely on: the declared range in the packaging metadata, the installer serving older users an earlier release, and the shims and matrix jobs a raise lets you delete.

for a senior

Bring the evidence: upstream end-of-life dates, install share for your project, your dependencies' floors, and the concrete maintenance cost of the oldest matrix job. Then execute the raise with the cleanup in the same change.

for a principal

Own the contract. Decide the review cadence, whether the previous line still gets security fixes and for how long, how it is announced, and how you answer the large dependent who asks you to wait.

### The question behind the question "When do we drop Python 3.10?" is really "what is the oldest interpreter still worth paying for?" It is a cost question with evidence on both sides, and the reason it is a lead's call rather than a maintainer's whim is that the cost is spread across users you cannot see. ### The evidence to gather **Upstream support.** CPython has shipped annually since PEP 602, and each minor line is supported for about five years — roughly two years of bugfix releases, then security-only fixes until end of life. That gives an unarguable outer boundary: 3.9 reached end of life in October 2025 and 3.10's security window closes around October 2026, so supporting either today means supporting an interpreter that will not receive even a security patch. It is the cleanest argument you have, because it is not about your preferences. **Install share.** Package indexes publish per-interpreter download telemetry, and it is the number that ends the argument. Telemetry is noisy — mirrors, CI runners and caches all inflate it — so read the trend rather than the level, and read it for *your* project rather than the ecosystem average, because a library used mostly inside long-lived enterprise images has a very different curve from a developer tool. **Your dependency floors.** You cannot support an interpreter that your own required dependencies have dropped; the resolver will simply fail to find versions of them, and the error will name them rather than you. Check the floors of everything you require before promising anything. **Your own cost.** Count the compatibility shims, the runtime guards, the conditional dependencies with markers, and the jobs in the matrix. A library rendering image thumbnails with a 340-case regression pack pays for that pack once per interpreter, on every push; and the oldest job is disproportionately the one that flakes — an occasional clock-skew artefact on the slowest runner — and disproportionately the one nobody wants to debug. That drag is genuine engineering time, and it should be stated in hours, not adjectives. **What the new floor buys.** Be concrete. If moving to 3.11 lets you delete an exception-aggregation shim in favour of native exception groups, or moving to 3.12 lets you use the type-parameter syntax and drop a compatibility import, say so. "We would like to use newer features" is not an argument; "this deletes 400 lines and two conditional dependencies" is. ### Why raising a floor is not abandonment The packaging metadata does the humane part for you. Once the new release declares a higher `requires-python`, an installer running on an older interpreter filters it out and resolves to your newest release that still allows it. Those users are not broken; they are frozen. That is the single most reassuring fact to bring to the discussion, and it is also why the floor must have been declared on the older releases too — the mechanism only works if the metadata was always there. What remains is a policy decision you own: does the last supporting line still receive security fixes, and for how long? For a widely-depended-on library the honest answers are usually "yes, for a stated window" or "no, and we say so plainly in the release notes." Silence is the bad option. ### How to execute it Raise the floor in a release whose version number signals a compatibility change under whatever versioning policy you publish, and announce it before it lands rather than in the changelog after. Land the deletions in the same change: shims, guards, conditional dependencies and matrix jobs should all disappear together, because a floor raised without the cleanup buys nothing and leaves the codebase claiming to support something it no longer tests. Add the new interpreter to the matrix before you remove the old one, so there is never a window where the top of the range is untested. ### The shape of the disagreement Expect the counter-argument to be about a specific dependent — a platform image, an enterprise environment, a downstream distribution frozen on an old interpreter. Take it seriously and make it concrete: who, how many, and for how long. Often the right answer is a stated support window on the old line rather than holding the whole project back, and occasionally it is that you are a leaf application rather than a library, in which case you are free to move immediately and the entire discussion is a five-minute one. ### The failure modes Moving too slowly accumulates shims that nobody understands and a matrix that costs more than the feature work. Moving too fast, without the metadata to catch old users, produces a wave of confusing installs on a release that cannot run. The discipline that avoids both is unremarkable: declare the floor from the first release, review it on a schedule tied to upstream end-of-life dates rather than to enthusiasm, and make the decision with numbers.

  • What actually happens to users below the new floor once you release?
    Their installer filters out every release whose declared range excludes them and resolves to your newest release that still allows their interpreter. They are frozen on that version, not broken, and they see a note listing the versions that were ignored. The mechanism only works if those older releases also carried the field, so declaring it from the first release is what makes a later raise safe.
  • How do you answer a large dependent that says it cannot move off the old interpreter yet?
    Make it concrete: who they are, how many, and their own timeline. Then choose between a stated support window on the previous line with security fixes only, or a firm date after which it is frozen. Both are defensible; holding the whole project on an end-of-life interpreter indefinitely for one unnamed dependent is not.
  • What should land in the same release as the raised floor?
    The cleanup it pays for: compatibility shims, sys.version_info guards, conditional dependencies with markers keyed to the dropped versions, and the matrix jobs for those interpreters. A floor raised without the deletions buys nothing and leaves code that still claims to handle interpreters nothing tests any more.

Raising a floor is like a bus route dropping its earliest stop: the people there are not stranded, they keep the old timetable, but somebody has to say so out loud and mean it.

saying these in an interview costs you the question

  • Raising the floor because the new release has appealing features
  • Believing old users are broken rather than frozen
  • Ignoring the floors your own dependencies already declare
  • Treating install telemetry as exact rather than as a trend
  • Raising the floor while leaving all the compatibility shims in place
  • Holding on an end-of-life interpreter for an unnamed dependent

context