Most of a Python 2→3 migration is mechanical, and 2to3 automates it: it turns print x into print(x), except E, e into except E as e, dict.has_key(k) into k in dict, xrange into range, and renamed imports like urllib2 into urllib.request. Paste your code into an online Python 2 to 3 converter (no install), or run the 2to3 CLI. The one thing it can't do for you is decide text vs bytes (str/unicode → str/bytes) — that needs a human. Always run your tests against the converted code.
Python 2 hit end-of-life on January 1, 2020 — no more security patches — and libraries, package managers, and OSes keep dropping support for it. If you still have Python 2 code, migrating isn't optional anymore. The good news: the bulk of the work is mechanical syntax changes that a tool can do for you.
Here's exactly what the standard 2to3 tool converts automatically (with a real before/after I ran), what it deliberately leaves for you, and the safest migration workflow. One important 2026 note up front: 2to3 and its lib2to3 engine were deprecated in Python 3.11 and removed in Python 3.13, so you now run it from an older Python or use an online converter that bundles it.
What 2to3 converts automatically (tested)
I ran 2to3 on a small but representative Python 2 script — print statements, a renamed import, has_key, xrange, and old exception syntax. Here's the actual diff it produced:
-import urllib2+import urllib.request, urllib.error, urllib.parsedef fetch(url):- print "fetching", url- return urllib2.urlopen(url).read()+ print("fetching", url)+ return urllib.request.urlopen(url).read()-if data.has_key("a"):- print data["a"]+if "a" in data:+ print(data["a"])-for i in xrange(3):+for i in range(3):-except ZeroDivisionError, e:- print "error:", e+except ZeroDivisionError as e:+ print("error:", e)
Every one of those is a change you'd otherwise make by hand, hundreds of times. I confirmed the converted file parses as valid Python 3. The common transformations 2to3 handles:
| Python 2 | Python 3 | Fixer |
|---|---|---|
print x | print(x) | |
except E, e: | except E as e: | except |
d.has_key(k) | k in d | has_key |
xrange(n) | range(n) | xrange |
import urllib2 | import urllib.request, ... | imports |
unicode / basestring | str | basestring |
raw_input() | input() | input |
Method 1 — the online converter (no install)
Because 2to3 is gone from modern Python, the easiest path is a browser tool. Paste your Python 2 source into the free Python 2 to 3 Converter and it applies the same fixers and hands back Python 3 — no need to keep an old Python around or install anything. It runs in your browser, so your code isn't uploaded.
Method 2 — the 2to3 CLI (from an older Python)
If you have a Python ≤3.12 install, the CLI still works and is handy for whole projects:
# preview the changes (prints a diff, edits nothing)$ 2to3 legacy.py# apply them in place (no .bak backup with -n)$ 2to3 -w -n legacy.py# a whole project$ 2to3 -w -n ./src
Since lib2to3 was removed in Python 3.13, run this from an older interpreter (e.g. a 3.11/3.12 venv). For a modern, maintained alternative, the 2to3-style fixers also live in tools like python-modernize, or just use the online converter above.
What 2to3 can’t do: text vs bytes
This is the part that actually requires thought, and no tool decides it for you. Python 2 blurred the line between str and unicode; Python 3 makes a hard distinction between `str` (text) and `bytes` (binary). Code that leaned on the old, loose behavior — reading files, network payloads, encoding/decoding — often needs a human to choose what should be text and what should be bytes.
2to3 will happily convert the *syntax* around your I/O, but it can't know that a socket payload should stay bytes while a config string should be str. Expect to hand-fix anything touching file encodings, network data, or binary blobs — that's where migrations actually break.
Other things to review by hand: integer vs float division in numeric code, dictionary keys()/items() now returning views (not lists), and any C-extension or library that itself only supported Python 2.
The safe migration workflow
- Get tests first. A codebase with decent test coverage migrates in an afternoon; one without needs careful manual verification. If you have no tests, write a few smoke tests before touching anything.
- Run 2to3 (or the online converter) to do the mechanical 90%.
- Run your test suite on the Python 3 result and fix what fails — most failures cluster around text/bytes.
- Review I/O and encodings by hand: file reads/writes, network payloads,
open()modes ('rb'vs'r'). - Format and sanity-check — tidy the result (e.g. with a formatter) and run it in an online Python 3 compiler to confirm it executes cleanly.
Convert Python 2 to 3 — free
Paste your Python 2 code and get Python 3 back instantly. In your browser, nothing uploaded, no old install needed.
Open the Python 2 to 3 ConverterFree tools mentioned here
Frequently asked questions
How do I convert Python 2 code to Python 3?
Run the standard 2to3 tool (or an online Python 2 to 3 converter) to automate the mechanical changes — print statements, exception syntax, dict methods, renamed imports, xrange. Then run your test suite on the result and hand-fix anything involving text vs bytes, since that's the part no tool can decide for you.
Is the 2to3 tool still available in Python 3.13+?
No. The 2to3 script and its lib2to3 engine were deprecated in Python 3.11 and removed in Python 3.13. To use it, run it from an older interpreter (3.11/3.12), or use an online converter that bundles the fixers, or a maintained alternative like python-modernize.
What does 2to3 not handle automatically?
The big one is the Python 3 split between str (text) and bytes (binary) — 2to3 converts surrounding syntax but can't decide which of your values should be text vs bytes, so file I/O, network payloads, and encoding code need manual review. Integer division nuances and dict keys()/items() returning views also warrant a check.
Is it safe to still run Python 2 code?
Not really. Python 2 reached end-of-life on January 1, 2020 and gets no security patches, and support is disappearing from libraries and operating systems. Migrating to Python 3 is important for both security and long-term compatibility.
How long does a Python 2 to 3 migration take?
It depends almost entirely on test coverage. With a good test suite, 2to3 does most of the work and you fix the text/bytes fallout in an afternoon for a typical project. Without tests, budget more time for manual verification, since you have no automated way to catch behavioral regressions.