Where Python 2 still appears and what to do about it
The places 2.7 survives, a checklist for deciding between converting and containing it, and how to keep the blast radius small.
Where it survives
| Situation | Why it persists | Realistic move |
|---|---|---|
| Embedded interpreters | Shipped inside a device or an application | Contain it; the vendor controls the runtime |
| Vendor SDKs | A hardware or payment vendor ships a 2.7 library | Isolate behind a service boundary and plan the exit |
| Frozen build systems | Old toolchain images nobody wants to touch | Freeze as a container image, never rebuild the host |
| Long-lived internal scripts | Nobody owns them and they still work | Inventory, then convert by risk, not by age |
| Scientific pipelines | Pinned to an old NumPy or SciPy | Port to a current stack; the numerical results rarely change |
The pattern is consistent: Python 2 persists in code that is coupled to something you do not control. That coupling, not the language version, is the actual problem to solve.
Convert or contain
Convert when:
- the code is still evolving and will receive features
- it handles untrusted input or network traffic
- it depends on libraries still available under Python 3
- the module has tests, or is small enough to characterise quickly
Contain when:
- a third-party binary requires the 2.7 interpreter
- the code is frozen and will never change
- migration cost exceeds the cost of running the risk, and the risk is bounded
- you can isolate it behind a network boundary- Containment means: its own container image, no inbound internet, no access to production data beyond a defined interface, and a documented owner.
- Conversion means: characterisation tests first, then a mechanical 2to3 pass, then the semantic changes the tools miss.
- Do not do both half-way. A module that half-supports both interpreters with
sixis the worst of both worlds until the port finishes.
⚠️
An unpatched interpreter exposed to a network is the single highest-risk item on this list. If a 2.7 service must accept traffic, terminate TLS at a maintained proxy, keep the process on a private network, and log every dependency you cannot patch — then put a date on the exit.
A migration plan that survives contact
# 1. inventory what you actually have
grep -rn "print " --include="*.py" . | wc -l
grep -rn "except .*," --include="*.py" .
grep -rn "__metaclass__\|iteritems\|has_key\|xrange\|raw_input\|urllib2" --include="*.py" .
# 2. measure the surface before promising a date
2to3 -f all -n -W app/ | grep -c "^---"- Inventory every Python 2 file, and every dependency that only ships a 2.7 release.
- Pin the current environment and capture a passing test run as the baseline.
- Convert leaf modules inward: the ones nothing else imports, so each step is independently shippable.
- Run the suite against both interpreters after each step, with
sixbridging the gap. - Remove
six, delete the Python 2 CI job, and add a CI rule that fails on a 2.7-only import.
FAQ
Is 2to3 reliable?
For syntax it is dependable:
print, integer division, the except form and the renamed modules are handled well. It cannot fix semantics such as integer division in money calculations, text handling or dict ordering, so review every hunk and rely on your characterisation tests.How long should we plan to keep the Python 2 service?
Long enough to build the replacement, short enough that you keep pressure on it. Give the service a named owner and a review date, otherwise containment quietly becomes permanent.
Related
Migrating to Python 3 Testing legacy Python 2 code before you change it
Last refreshed 2026-09-18.