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

SituationWhy it persistsRealistic move
Embedded interpretersShipped inside a device or an applicationContain it; the vendor controls the runtime
Vendor SDKsA hardware or payment vendor ships a 2.7 libraryIsolate behind a service boundary and plan the exit
Frozen build systemsOld toolchain images nobody wants to touchFreeze as a container image, never rebuild the host
Long-lived internal scriptsNobody owns them and they still workInventory, then convert by risk, not by age
Scientific pipelinesPinned to an old NumPy or SciPyPort 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 six is 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 "^---"
  1. Inventory every Python 2 file, and every dependency that only ships a 2.7 release.
  2. Pin the current environment and capture a passing test run as the baseline.
  3. Convert leaf modules inward: the ones nothing else imports, so each step is independently shippable.
  4. Run the suite against both interpreters after each step, with six bridging the gap.
  5. 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.

Migrating to Python 3 Testing legacy Python 2 code before you change it

Last refreshed 2026-09-18.